summaryrefslogtreecommitdiff
path: root/src/modlistviewactions.cpp
diff options
context:
space:
mode:
authorMikaƫl Capelle <capelle.mikael@gmail.com>2021-01-10 10:25:37 +0100
committerGitHub <noreply@github.com>2021-01-10 10:25:37 +0100
commit80e44a9e3ade61695b4807a3a900d2866138ecac (patch)
tree1ef9904664d8d34dba6692bbf5325aea8c199d08 /src/modlistviewactions.cpp
parenteaec140f7c823012c09536175d8917dddcacdb7c (diff)
parent4a0ce804ea486744e5f6140a0ce4538d99e21ce3 (diff)
Merge pull request #1338 from Holt59/collapsible-separators
Collapsible separators (and refactoring).
Diffstat (limited to 'src/modlistviewactions.cpp')
-rw-r--r--src/modlistviewactions.cpp1142
1 files changed, 1142 insertions, 0 deletions
diff --git a/src/modlistviewactions.cpp b/src/modlistviewactions.cpp
new file mode 100644
index 00000000..9957618a
--- /dev/null
+++ b/src/modlistviewactions.cpp
@@ -0,0 +1,1142 @@
+#include "modlistviewactions.h"
+
+#include <QGridLayout>
+#include <QGroupBox>
+#include <QInputDialog>
+#include <QLabel>
+#include <QRegExp>
+
+#include <log.h>
+#include <report.h>
+#include <utility.h>
+
+#include "categories.h"
+#include "filedialogmemory.h"
+#include "filterlist.h"
+#include "listdialog.h"
+#include "modinfodialog.h"
+#include "modlist.h"
+#include "modlistview.h"
+#include "messagedialog.h"
+#include "nexusinterface.h"
+#include "nxmaccessmanager.h"
+#include "savetextasdialog.h"
+#include "organizercore.h"
+#include "overwriteinfodialog.h"
+#include "csvbuilder.h"
+#include "pluginlistview.h"
+#include "shared/filesorigin.h"
+#include "shared/directoryentry.h"
+#include "shared/fileregister.h"
+#include "directoryrefresher.h"
+
+using namespace MOBase;
+using namespace MOShared;
+
+
+ModListViewActions::ModListViewActions(
+ OrganizerCore& core, FilterList& filters, CategoryFactory& categoryFactory,
+ ModListView* view, PluginListView* pluginView, QObject* nxmReceiver) :
+ QObject(view)
+ , m_core(core)
+ , m_filters(filters)
+ , m_categories(categoryFactory)
+ , m_view(view)
+ , m_pluginView(pluginView)
+ , m_parent(view->topLevelWidget())
+ , m_receiver(nxmReceiver)
+{
+
+}
+
+void ModListViewActions::installMod(const QString& archivePath) const
+{
+ try {
+ QString path = archivePath;
+ if (path.isEmpty()) {
+ QStringList extensions = m_core.installationManager()->getSupportedExtensions();
+ for (auto iter = extensions.begin(); iter != extensions.end(); ++iter) {
+ *iter = "*." + *iter;
+ }
+
+ path = FileDialogMemory::getOpenFileName("installMod", m_parent, tr("Choose Mod"), QString(),
+ tr("Mod Archive").append(QString(" (%1)").arg(extensions.join(" "))));
+ }
+
+ if (path.isEmpty()) {
+ return;
+ }
+ else {
+ m_core.installMod(path, false, nullptr, QString());
+ }
+ }
+ catch (const std::exception& e) {
+ reportError(e.what());
+ }
+}
+
+void ModListViewActions::createEmptyMod(const QModelIndex& index) const
+{
+ GuessedValue<QString> name;
+ name.setFilter(&fixDirectoryName);
+
+ while (name->isEmpty()) {
+ bool ok;
+ name.update(QInputDialog::getText(m_parent, tr("Create Mod..."),
+ tr("This will create an empty mod.\n"
+ "Please enter a name:"), QLineEdit::Normal, "", &ok),
+ GUESS_USER);
+ if (!ok) {
+ return;
+ }
+ }
+
+ if (m_core.modList()->getMod(name) != nullptr) {
+ reportError(tr("A mod with this name already exists"));
+ return;
+ }
+
+ int newPriority = -1;
+ if (index.isValid() && m_view->sortColumn() == ModList::COL_PRIORITY) {
+ newPriority = m_core.currentProfile()->getModPriority(index.data(ModList::IndexRole).toInt());
+ }
+
+ IModInterface* newMod = m_core.createMod(name);
+ if (newMod == nullptr) {
+ return;
+ }
+
+ m_core.refresh();
+
+ if (newPriority >= 0) {
+ m_core.modList()->changeModPriority(ModInfo::getIndex(name), newPriority);
+ }
+}
+
+void ModListViewActions::createSeparator(const QModelIndex& index) const
+{
+ GuessedValue<QString> name;
+ name.setFilter(&fixDirectoryName);
+ while (name->isEmpty()) {
+ bool ok;
+ name.update(QInputDialog::getText(m_parent, tr("Create Separator..."),
+ tr("This will create a new separator.\n"
+ "Please enter a name:"), QLineEdit::Normal, "", &ok),
+ GUESS_USER);
+ if (!ok) { return; }
+ }
+ if (m_core.modList()->getMod(name) != nullptr) {
+ reportError(tr("A separator with this name already exists"));
+ return;
+ }
+ name->append("_separator");
+ if (m_core.modList()->getMod(name) != nullptr) {
+ return;
+ }
+
+ int newPriority = -1;
+ if (index.isValid() && m_view->sortColumn() == ModList::COL_PRIORITY) {
+ newPriority = m_core.currentProfile()->getModPriority(index.data(ModList::IndexRole).toInt());
+ }
+
+ if (m_core.createMod(name) == nullptr) { return; }
+ m_core.refresh();
+
+ if (newPriority >= 0) {
+ m_core.modList()->changeModPriority(ModInfo::getIndex(name), newPriority);
+ }
+
+ if (auto c = m_core.settings().colors().previousSeparatorColor()) {
+ ModInfo::getByIndex(ModInfo::getIndex(name))->setColor(*c);
+ }
+}
+
+void ModListViewActions::checkModsForUpdates() const
+{
+ bool checkingModsForUpdate = false;
+ if (NexusInterface::instance().getAccessManager()->validated()) {
+ checkingModsForUpdate = ModInfo::checkAllForUpdate(&m_core.pluginContainer(), m_receiver);
+ NexusInterface::instance().requestEndorsementInfo(m_receiver, QVariant(), QString());
+ NexusInterface::instance().requestTrackingInfo(m_receiver, QVariant(), QString());
+ } else {
+ QString apiKey;
+ if (GlobalSettings::nexusApiKey(apiKey)) {
+ m_core.doAfterLogin([=] () { checkModsForUpdates(); });
+ NexusInterface::instance().getAccessManager()->apiCheck(apiKey);
+ } else {
+ log::warn("{}", tr("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus."));
+ }
+ }
+
+ bool updatesAvailable = false;
+ for (auto mod : m_core.modList()->allMods()) {
+ ModInfo::Ptr modInfo = ModInfo::getByName(mod);
+ if (modInfo->updateAvailable()) {
+ updatesAvailable = true;
+ break;
+ }
+ }
+
+ if (updatesAvailable || checkingModsForUpdate) {
+ m_view->setFilterCriteria({{
+ ModListSortProxy::TypeSpecial,
+ CategoryFactory::UpdateAvailable,
+ false}
+ });
+
+ m_filters.setSelection({{
+ ModListSortProxy::TypeSpecial,
+ CategoryFactory::UpdateAvailable,
+ false
+ }});
+ }
+}
+
+void ModListViewActions::checkModsForUpdates(std::multimap<QString, int> const& IDs) const
+{
+ if (m_core.settings().network().offlineMode()) {
+ return;
+ }
+
+ if (NexusInterface::instance().getAccessManager()->validated()) {
+ ModInfo::manualUpdateCheck(m_receiver, IDs);
+ }
+ else {
+ QString apiKey;
+ if (GlobalSettings::nexusApiKey(apiKey)) {
+ m_core.doAfterLogin([=]() { checkModsForUpdates(IDs); });
+ NexusInterface::instance().getAccessManager()->apiCheck(apiKey);
+ }
+ else
+ log::warn("{}", tr("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus."));
+ }
+}
+
+void ModListViewActions::checkModsForUpdates(const QModelIndexList& indices) const
+{
+ std::multimap<QString, int> ids;
+ for (auto& idx : indices) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt());
+ ids.insert(std::make_pair<QString, int>(info->gameName(), info->nexusId()));
+ }
+ checkModsForUpdates(ids);
+}
+
+void ModListViewActions::exportModListCSV() const
+{
+ QDialog selection(m_parent);
+ QGridLayout* grid = new QGridLayout;
+ selection.setWindowTitle(tr("Export to csv"));
+
+ QLabel* csvDescription = new QLabel();
+ csvDescription->setText(tr("CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet.\nYou can also use online editors and converters instead."));
+ grid->addWidget(csvDescription);
+
+ QGroupBox* groupBoxRows = new QGroupBox(tr("Select what mods you want export:"));
+ QRadioButton* all = new QRadioButton(tr("All installed mods"));
+ QRadioButton* active = new QRadioButton(tr("Only active (checked) mods from your current profile"));
+ QRadioButton* visible = new QRadioButton(tr("All currently visible mods in the mod list"));
+
+ QVBoxLayout* vbox = new QVBoxLayout;
+ vbox->addWidget(all);
+ vbox->addWidget(active);
+ vbox->addWidget(visible);
+ vbox->addStretch(1);
+ groupBoxRows->setLayout(vbox);
+
+ grid->addWidget(groupBoxRows);
+
+ QButtonGroup* buttonGroupRows = new QButtonGroup();
+ buttonGroupRows->addButton(all, 0);
+ buttonGroupRows->addButton(active, 1);
+ buttonGroupRows->addButton(visible, 2);
+ buttonGroupRows->button(0)->setChecked(true);
+
+ QGroupBox* groupBoxColumns = new QGroupBox(tr("Choose what Columns to export:"));
+ groupBoxColumns->setFlat(true);
+
+ QCheckBox* mod_Priority = new QCheckBox(tr("Mod_Priority"));
+ mod_Priority->setChecked(true);
+ QCheckBox* mod_Name = new QCheckBox(tr("Mod_Name"));
+ mod_Name->setChecked(true);
+ QCheckBox* mod_Note = new QCheckBox(tr("Notes_column"));
+ QCheckBox* mod_Status = new QCheckBox(tr("Mod_Status"));
+ mod_Status->setChecked(true);
+ QCheckBox* primary_Category = new QCheckBox(tr("Primary_Category"));
+ QCheckBox* nexus_ID = new QCheckBox(tr("Nexus_ID"));
+ QCheckBox* mod_Nexus_URL = new QCheckBox(tr("Mod_Nexus_URL"));
+ QCheckBox* mod_Version = new QCheckBox(tr("Mod_Version"));
+ QCheckBox* install_Date = new QCheckBox(tr("Install_Date"));
+ QCheckBox* download_File_Name = new QCheckBox(tr("Download_File_Name"));
+
+ QVBoxLayout* vbox1 = new QVBoxLayout;
+ vbox1->addWidget(mod_Priority);
+ vbox1->addWidget(mod_Name);
+ vbox1->addWidget(mod_Status);
+ vbox1->addWidget(mod_Note);
+ vbox1->addWidget(primary_Category);
+ vbox1->addWidget(nexus_ID);
+ vbox1->addWidget(mod_Nexus_URL);
+ vbox1->addWidget(mod_Version);
+ vbox1->addWidget(install_Date);
+ vbox1->addWidget(download_File_Name);
+ groupBoxColumns->setLayout(vbox1);
+
+ grid->addWidget(groupBoxColumns);
+
+ QPushButton* ok = new QPushButton("Ok");
+ QPushButton* cancel = new QPushButton("Cancel");
+ QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+
+ connect(buttons, SIGNAL(accepted()), &selection, SLOT(accept()));
+ connect(buttons, SIGNAL(rejected()), &selection, SLOT(reject()));
+
+ grid->addWidget(buttons);
+
+ selection.setLayout(grid);
+
+
+ if (selection.exec() == QDialog::Accepted) {
+
+ unsigned int numMods = ModInfo::getNumMods();
+ int selectedRowID = buttonGroupRows->checkedId();
+
+ try {
+ QBuffer buffer;
+ buffer.open(QIODevice::ReadWrite);
+ CSVBuilder builder(&buffer);
+ builder.setEscapeMode(CSVBuilder::TYPE_STRING, CSVBuilder::QUOTE_ALWAYS);
+ std::vector<std::pair<QString, CSVBuilder::EFieldType> > fields;
+ if (mod_Priority->isChecked())
+ fields.push_back(std::make_pair(QString("#Mod_Priority"), CSVBuilder::TYPE_STRING));
+ if (mod_Status->isChecked())
+ fields.push_back(std::make_pair(QString("#Mod_Status"), CSVBuilder::TYPE_STRING));
+ if (mod_Name->isChecked())
+ fields.push_back(std::make_pair(QString("#Mod_Name"), CSVBuilder::TYPE_STRING));
+ if (mod_Note->isChecked())
+ fields.push_back(std::make_pair(QString("#Note"), CSVBuilder::TYPE_STRING));
+ if (primary_Category->isChecked())
+ fields.push_back(std::make_pair(QString("#Primary_Category"), CSVBuilder::TYPE_STRING));
+ if (nexus_ID->isChecked())
+ fields.push_back(std::make_pair(QString("#Nexus_ID"), CSVBuilder::TYPE_INTEGER));
+ if (mod_Nexus_URL->isChecked())
+ fields.push_back(std::make_pair(QString("#Mod_Nexus_URL"), CSVBuilder::TYPE_STRING));
+ if (mod_Version->isChecked())
+ fields.push_back(std::make_pair(QString("#Mod_Version"), CSVBuilder::TYPE_STRING));
+ if (install_Date->isChecked())
+ fields.push_back(std::make_pair(QString("#Install_Date"), CSVBuilder::TYPE_STRING));
+ if (download_File_Name->isChecked())
+ fields.push_back(std::make_pair(QString("#Download_File_Name"), CSVBuilder::TYPE_STRING));
+
+ builder.setFields(fields);
+
+ builder.writeHeader();
+
+ auto indexesByPriority = m_core.currentProfile()->getAllIndexesByPriority();
+ for (auto& iter : indexesByPriority) {
+ ModInfo::Ptr info = ModInfo::getByIndex(iter.second);
+ bool enabled = m_core.currentProfile()->modEnabled(iter.second);
+ if ((selectedRowID == 1) && !enabled) {
+ continue;
+ }
+ else if ((selectedRowID == 2) && !m_view->isModVisible(iter.second)) {
+ continue;
+ }
+ std::vector<ModInfo::EFlag> flags = info->getFlags();
+ if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end()) &&
+ (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) == flags.end())) {
+ if (mod_Priority->isChecked())
+ builder.setRowField("#Mod_Priority", QString("%1").arg(iter.first, 4, 10, QChar('0')));
+ if (mod_Status->isChecked())
+ builder.setRowField("#Mod_Status", (enabled) ? "+" : "-");
+ if (mod_Name->isChecked())
+ builder.setRowField("#Mod_Name", info->name());
+ if (mod_Note->isChecked())
+ builder.setRowField("#Note", QString("%1").arg(info->comments().remove(',')));
+ if (primary_Category->isChecked())
+ builder.setRowField("#Primary_Category", (m_categories.categoryExists(info->primaryCategory())) ? m_categories.getCategoryNameByID(info->primaryCategory()) : "");
+ if (nexus_ID->isChecked())
+ builder.setRowField("#Nexus_ID", info->nexusId());
+ if (mod_Nexus_URL->isChecked())
+ builder.setRowField("#Mod_Nexus_URL", (info->nexusId() > 0) ? NexusInterface::instance().getModURL(info->nexusId(), info->gameName()) : "");
+ if (mod_Version->isChecked())
+ builder.setRowField("#Mod_Version", info->version().canonicalString());
+ if (install_Date->isChecked())
+ builder.setRowField("#Install_Date", info->creationTime().toString("yyyy/MM/dd HH:mm:ss"));
+ if (download_File_Name->isChecked())
+ builder.setRowField("#Download_File_Name", info->installationFile());
+
+ builder.writeRow();
+ }
+ }
+
+ SaveTextAsDialog saveDialog(m_parent);
+ saveDialog.setText(buffer.data());
+ saveDialog.exec();
+ }
+ catch (const std::exception& e) {
+ reportError(tr("export failed: %1").arg(e.what()));
+ }
+ }
+}
+
+void ModListViewActions::displayModInformation(const QString& modName, ModInfoTabIDs tab) const
+{
+ unsigned int index = ModInfo::getIndex(modName);
+ if (index == UINT_MAX) {
+ log::error("failed to resolve mod name {}", modName);
+ return;
+ }
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
+ displayModInformation(modInfo, index, tab);
+}
+
+void ModListViewActions::displayModInformation(unsigned int index, ModInfoTabIDs tab) const
+{
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
+ displayModInformation(modInfo, index, tab);
+}
+
+void ModListViewActions::displayModInformation(ModInfo::Ptr modInfo, unsigned int modIndex, ModInfoTabIDs tab) const
+{
+ if (!m_core.modList()->modInfoAboutToChange(modInfo)) {
+ log::debug("a different mod information dialog is open. If this is incorrect, please restart MO");
+ return;
+ }
+ std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) {
+ QDialog* dialog = m_parent->findChild<QDialog*>("__overwriteDialog");
+ try {
+ if (dialog == nullptr) {
+ dialog = new OverwriteInfoDialog(modInfo, m_parent);
+ dialog->setObjectName("__overwriteDialog");
+ }
+ else {
+ qobject_cast<OverwriteInfoDialog*>(dialog)->setModInfo(modInfo);
+ }
+
+ dialog->show();
+ dialog->raise();
+ dialog->activateWindow();
+ connect(dialog, &QDialog::finished, [=]() {
+ m_core.modList()->modInfoChanged(modInfo);
+ dialog->deleteLater();
+ m_core.refreshDirectoryStructure();
+ });
+ }
+ catch (const std::exception& e) {
+ reportError(tr("Failed to display overwrite dialog: %1").arg(e.what()));
+ }
+ }
+ else {
+ modInfo->saveMeta();
+
+ ModInfoDialog dialog(m_core, m_core.pluginContainer(), modInfo, m_view, m_parent);
+ connect(&dialog, &ModInfoDialog::originModified, this, &ModListViewActions::originModified);
+ connect(&dialog, &ModInfoDialog::modChanged, [=](unsigned int index) {
+ auto idx = m_view->indexModelToView(m_core.modList()->index(index, 0));
+ m_view->selectionModel()->select(idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
+ m_view->scrollTo(idx);
+ });
+
+ //Open the tab first if we want to use the standard indexes of the tabs.
+ if (tab != ModInfoTabIDs::None) {
+ dialog.selectTab(tab);
+ }
+
+ dialog.exec();
+
+ modInfo->saveMeta();
+ m_core.modList()->modInfoChanged(modInfo);
+ }
+
+ if (m_core.currentProfile()->modEnabled(modIndex) && !modInfo->isForeign()) {
+ FilesOrigin& origin = m_core.directoryStructure()->getOriginByName(ToWString(modInfo->name()));
+ origin.enable(false);
+
+ if (m_core.directoryStructure()->originExists(ToWString(modInfo->name()))) {
+ FilesOrigin& origin = m_core.directoryStructure()->getOriginByName(ToWString(modInfo->name()));
+ origin.enable(false);
+
+ m_core.directoryRefresher()->addModToStructure(m_core.directoryStructure()
+ , modInfo->name()
+ , m_core.currentProfile()->getModPriority(modIndex)
+ , modInfo->absolutePath()
+ , modInfo->stealFiles()
+ , modInfo->archives());
+ DirectoryRefresher::cleanStructure(m_core.directoryStructure());
+ m_core.directoryStructure()->getFileRegister()->sortOrigins();
+ m_core.refreshLists();
+ }
+ }
+}
+
+void ModListViewActions::sendModsToTop(const QModelIndexList& index) const
+{
+ m_core.modList()->changeModsPriority(index, 0);
+}
+
+void ModListViewActions::sendModsToBottom(const QModelIndexList& index) const
+{
+ m_core.modList()->changeModsPriority(index, std::numeric_limits<int>::max());
+}
+
+void ModListViewActions::sendModsToPriority(const QModelIndexList& index) const
+{
+ bool ok;
+ int priority = QInputDialog::getInt(m_parent,
+ tr("Set Priority"), tr("Set the priority of the selected mods"),
+ 0, 0, std::numeric_limits<int>::max(), 1, &ok);
+ if (!ok) return;
+
+ m_core.modList()->changeModsPriority(index, priority);
+}
+
+void ModListViewActions::sendModsToSeparator(const QModelIndexList& index) const
+{
+ QStringList separators;
+ auto indexesByPriority = m_core.currentProfile()->getAllIndexesByPriority();
+ for (auto iter = indexesByPriority.begin(); iter != indexesByPriority.end(); iter++) {
+ if ((iter->second != UINT_MAX)) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(iter->second);
+ if (modInfo->isSeparator()) {
+ separators << modInfo->name().chopped(10); // Chops the "_separator" away from the name
+ }
+ }
+ }
+
+ ListDialog dialog(m_parent);
+ dialog.setWindowTitle("Select a separator...");
+ dialog.setChoices(separators);
+
+ if (dialog.exec() == QDialog::Accepted) {
+ QString result = dialog.getChoice();
+ if (!result.isEmpty()) {
+ result += "_separator";
+
+ int newPriority = std::numeric_limits<int>::max();
+ bool foundSection = false;
+ for (auto mod : m_core.modsSortedByProfilePriority(m_core.currentProfile())) {
+ unsigned int modIndex = ModInfo::getIndex(mod);
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+ if (!foundSection && result.compare(mod) == 0) {
+ foundSection = true;
+ }
+ else if (foundSection && modInfo->isSeparator()) {
+ newPriority = m_core.currentProfile()->getModPriority(modIndex);
+ break;
+ }
+ }
+
+ if (index.size() == 1
+ && m_core.currentProfile()->getModPriority(index[0].data(ModList::IndexRole).toInt()) < newPriority) {
+ --newPriority;
+ }
+
+ m_core.modList()->changeModsPriority(index, newPriority);
+ }
+ }
+}
+
+void ModListViewActions::renameMod(const QModelIndex& index) const
+{
+ try {
+ m_view->edit(m_view->indexModelToView(index));
+ }
+ catch (const std::exception& e) {
+ reportError(tr("failed to rename mod: %1").arg(e.what()));
+ }
+}
+
+void ModListViewActions::removeMods(const QModelIndexList& indices) const
+{
+ const int max_items = 20;
+
+ try {
+ if (indices.size() > 1) {
+ QString mods;
+ QStringList modNames;
+
+ int i = 0;
+ for (auto& idx : indices) {
+ QString name = idx.data().toString();
+ if (!ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt())->isRegular()) {
+ continue;
+ }
+
+ // adds an item for the mod name until `i` reaches `max_items`, which
+ // adds one "..." item; subsequent mods are not shown on the list but
+ // are still added to `modNames` below so they can be removed correctly
+
+ if (i < max_items) {
+ mods += "<li>" + name + "</li>";
+ }
+ else if (i == max_items) {
+ mods += "<li>...</li>";
+ }
+
+ modNames.append(ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt())->name());
+ ++i;
+ }
+ if (QMessageBox::question(m_parent, tr("Confirm"),
+ tr("Remove the following mods?<br><ul>%1</ul>").arg(mods),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ // use mod names instead of indexes because those become invalid during the removal
+ DownloadManager::startDisableDirWatcher();
+ for (QString name : modNames) {
+ m_core.modList()->removeRowForce(ModInfo::getIndex(name), QModelIndex());
+ }
+ DownloadManager::endDisableDirWatcher();
+ }
+ }
+ else if (!indices.isEmpty()) {
+ m_core.modList()->removeRow(indices[0].data(ModList::IndexRole).toInt(), QModelIndex());
+ }
+ m_view->updateModCount();
+ m_pluginView->updatePluginCount();
+ }
+ catch (const std::exception& e) {
+ reportError(tr("failed to remove mod: %1").arg(e.what()));
+ }
+}
+
+void ModListViewActions::ignoreMissingData(const QModelIndexList& indices) const
+{
+ for (auto& idx : indices) {
+ int row_idx = idx.data(ModList::IndexRole).toInt();
+ ModInfo::Ptr info = ModInfo::getByIndex(row_idx);
+ info->markValidated(true);
+ m_core.modList()->notifyChange(row_idx);
+ }
+}
+
+void ModListViewActions::setIgnoreUpdate(const QModelIndexList& indices, bool ignore) const
+{
+ for (auto& idx : indices) {
+ int modIdx = idx.data(ModList::IndexRole).toInt();
+ ModInfo::Ptr info = ModInfo::getByIndex(modIdx);
+ info->ignoreUpdate(ignore);
+ m_core.modList()->notifyChange(modIdx);
+ }
+}
+
+void ModListViewActions::changeVersioningScheme(const QModelIndex& index) const {
+ if (QMessageBox::question(m_parent, tr("Continue?"),
+ tr("The versioning scheme decides which version is considered newer than another.\n"
+ "This function will guess the versioning scheme under the assumption that the installed version is outdated."),
+ QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Yes) {
+
+ ModInfo::Ptr info = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt());
+
+ bool success = false;
+
+ static VersionInfo::VersionScheme schemes[] = { VersionInfo::SCHEME_REGULAR, VersionInfo::SCHEME_DECIMALMARK, VersionInfo::SCHEME_NUMBERSANDLETTERS };
+
+ for (int i = 0; i < sizeof(schemes) / sizeof(VersionInfo::VersionScheme) && !success; ++i) {
+ VersionInfo verOld(info->version().canonicalString(), schemes[i]);
+ VersionInfo verNew(info->newestVersion().canonicalString(), schemes[i]);
+ if (verOld < verNew) {
+ info->setVersion(verOld);
+ info->setNewestVersion(verNew);
+ success = true;
+ }
+ }
+ if (!success) {
+ QMessageBox::information(m_parent, tr("Sorry"),
+ tr("I don't know a versioning scheme where %1 is newer than %2.").arg(info->newestVersion().canonicalString()).arg(info->version().canonicalString()),
+ QMessageBox::Ok);
+ }
+ }
+}
+
+void ModListViewActions::markConverted(const QModelIndexList& indices) const
+{
+ for (auto& idx : indices) {
+ int modIdx = idx.data(ModList::IndexRole).toInt();
+ ModInfo::Ptr info = ModInfo::getByIndex(modIdx);
+ info->markConverted(true);
+ m_core.modList()->notifyChange(modIdx);
+ }
+}
+
+void ModListViewActions::visitOnNexus(const QModelIndexList& indices) const
+{
+ if (indices.size() > 10) {
+ if (QMessageBox::question(m_parent, tr("Opening Nexus Links"),
+ tr("You are trying to open %1 links to Nexus Mods. Are you sure you want to do this?").arg(indices.size()),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+ }
+
+ for (auto& idx : indices) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt());
+ int modID = info->nexusId();
+ QString gameName = info->gameName();
+ if (modID > 0) {
+ shell::Open(QUrl(NexusInterface::instance().getModURL(modID, gameName)));
+ }
+ else {
+ log::error("mod '{}' has no nexus id", info->name());
+ }
+ }
+}
+
+void ModListViewActions::visitWebPage(const QModelIndexList& indices) const
+{
+ if (indices.size() > 10) {
+ if (QMessageBox::question(m_parent, tr("Opening Web Pages"),
+ tr("You are trying to open %1 Web Pages. Are you sure you want to do this?").arg(indices.size()),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+ }
+
+ for (auto& idx : indices) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt());
+
+ const auto url = info->parseCustomURL();
+ if (url.isValid()) {
+ shell::Open(url);
+ }
+ }
+}
+
+void ModListViewActions::visitNexusOrWebPage(const QModelIndexList& indices) const
+{
+ if (indices.size() > 10) {
+ if (QMessageBox::question(m_parent, tr("Opening Web Pages"),
+ tr("You are trying to open %1 Web Pages. Are you sure you want to do this?").arg(indices.size()),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+ }
+
+ for (auto& idx : indices) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt());
+ if (!info) {
+ log::error("mod {} not found", idx.data(ModList::IndexRole).toInt());
+ continue;
+ }
+
+ int modID = info->nexusId();
+ QString gameName = info->gameName();
+ const auto url = info->parseCustomURL();
+
+ if (modID > 0) {
+ shell::Open(QUrl(NexusInterface::instance().getModURL(modID, gameName)));
+ }
+ else if (url.isValid()) {
+ shell::Open(url);
+ }
+ else {
+ log::error("mod '{}' has no valid link", info->name());
+ }
+ }
+}
+
+void ModListViewActions::reinstallMod(const QModelIndex& index) const
+{
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt());
+ QString installationFile = modInfo->installationFile();
+ if (installationFile.length() != 0) {
+ QString fullInstallationFile;
+ QFileInfo fileInfo(installationFile);
+ if (fileInfo.isAbsolute()) {
+ if (fileInfo.exists()) {
+ fullInstallationFile = installationFile;
+ }
+ else {
+ fullInstallationFile = m_core.downloadManager()->getOutputDirectory() + "/" + fileInfo.fileName();
+ }
+ }
+ else {
+ fullInstallationFile = m_core.downloadManager()->getOutputDirectory() + "/" + installationFile;
+ }
+ if (QFile::exists(fullInstallationFile)) {
+ m_core.installMod(fullInstallationFile, true, modInfo, modInfo->name());
+ }
+ else {
+ QMessageBox::information(m_parent, tr("Failed"), tr("Installation file no longer exists"));
+ }
+ }
+ else {
+ QMessageBox::information(m_parent, tr("Failed"),
+ tr("Mods installed with old versions of MO can't be reinstalled in this way."));
+ }
+}
+
+void ModListViewActions::createBackup(const QModelIndex& index) const
+{
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt());
+ QString backupDirectory = m_core.installationManager()->generateBackupName(modInfo->absolutePath());
+ if (!copyDir(modInfo->absolutePath(), backupDirectory, false)) {
+ QMessageBox::information(m_parent, tr("Failed"),
+ tr("Failed to create backup."));
+ }
+ m_core.refresh();
+ m_view->updateModCount();
+}
+
+void ModListViewActions::restoreHiddenFiles(const QModelIndexList& indices) const
+{
+ const int max_items = 20;
+
+ QFlags<FileRenamer::RenameFlags> flags = FileRenamer::UNHIDE;
+ flags |= FileRenamer::MULTIPLE;
+
+ FileRenamer renamer(m_parent, flags);
+
+ FileRenamer::RenameResults result = FileRenamer::RESULT_OK;
+
+ // multi selection
+ if (indices.size() > 1) {
+
+ QStringList modNames;
+ for (auto& idx : indices) {
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt());
+ const auto flags = modInfo->getFlags();
+
+ if (!modInfo->isRegular() ||
+ std::find(flags.begin(), flags.end(), ModInfo::FLAG_HIDDEN_FILES) == flags.end()) {
+ continue;
+ }
+
+ modNames.append(idx.data(Qt::DisplayRole).toString());
+ }
+
+ QString mods = "<li>" + modNames.mid(0, max_items).join("</li><li>") + "</li>";
+ if (modNames.size() > max_items) {
+ mods += "<li>...</li>";
+ }
+
+ if (QMessageBox::question(m_parent, tr("Confirm"),
+ tr("Restore all hidden files in the following mods?<br><ul>%1</ul>").arg(mods),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+
+ for (auto& idx : indices) {
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt());
+
+ const auto flags = modInfo->getFlags();
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_HIDDEN_FILES) != flags.end()) {
+ const QString modDir = modInfo->absolutePath();
+
+ auto partialResult = restoreHiddenFilesRecursive(renamer, modDir);
+
+ if (partialResult == FileRenamer::RESULT_CANCEL) {
+ result = FileRenamer::RESULT_CANCEL;
+ break;
+ }
+ emit originModified((m_core.directoryStructure()->getOriginByName(
+ ToWString(modInfo->internalName()))).getID());
+ }
+ }
+ }
+ }
+ else if (!indices.isEmpty()) {
+ //single selection
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(indices[0].data(ModList::IndexRole).toInt());
+ const QString modDir = modInfo->absolutePath();
+
+ if (QMessageBox::question(m_parent, tr("Are you sure?"),
+ tr("About to restore all hidden files in:\n") + modInfo->name(),
+ QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) {
+
+ result = restoreHiddenFilesRecursive(renamer, modDir);
+
+ emit originModified((m_core.directoryStructure()->getOriginByName(
+ ToWString(modInfo->internalName()))).getID());
+ }
+ }
+
+ if (result == FileRenamer::RESULT_CANCEL) {
+ log::debug("Restoring hidden files operation cancelled");
+ }
+ else {
+ log::debug("Finished restoring hidden files");
+ }
+}
+
+void ModListViewActions::setTracked(const QModelIndexList& indices, bool tracked) const
+{
+ m_core.loggedInAction(m_parent, [=] {
+ for (auto& idx : indices) {
+ ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt())->track(tracked);
+ }
+ });
+}
+
+void ModListViewActions::setEndorsed(const QModelIndexList& indices, bool endorsed) const
+{
+ m_core.loggedInAction(m_parent, [=] {
+ if (indices.size() > 1) {
+ MessageDialog::showMessage(tr("Endorsing multiple mods will take a while. Please wait..."), m_parent);
+ }
+
+ for (auto& idx : indices) {
+ ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt())->endorse(endorsed);
+ }
+ });
+}
+
+void ModListViewActions::willNotEndorsed(const QModelIndexList& indices) const
+{
+ for (auto& idx : indices) {
+ ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt())->setNeverEndorse();
+ }
+}
+
+void ModListViewActions::setColor(const QModelIndexList& indices, const QModelIndex& refIndex) const
+{
+ auto& settings = m_core.settings();
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(refIndex.data(ModList::IndexRole).toInt());
+
+ QColorDialog dialog(m_parent);
+ dialog.setOption(QColorDialog::ShowAlphaChannel);
+
+ QColor currentColor = modInfo->color();
+ if (currentColor.isValid()) {
+ dialog.setCurrentColor(currentColor);
+ }
+ else if (auto c = settings.colors().previousSeparatorColor()) {
+ dialog.setCurrentColor(*c);
+ }
+
+ if (!dialog.exec())
+ return;
+
+ currentColor = dialog.currentColor();
+ if (!currentColor.isValid())
+ return;
+
+ settings.colors().setPreviousSeparatorColor(currentColor);
+
+ for (auto& idx : indices) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt());
+ info->setColor(currentColor);
+ }
+
+}
+
+void ModListViewActions::resetColor(const QModelIndexList& indices) const
+{
+ for (auto& idx : indices) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt());
+ info->setColor(QColor());
+ }
+ m_core.settings().colors().removePreviousSeparatorColor();
+}
+
+void ModListViewActions::setCategories(ModInfo::Ptr mod, const std::vector<std::pair<int, bool>>& categories) const
+{
+ for (auto& [id, enabled] : categories) {
+ mod->setCategory(id, enabled);
+ }
+}
+
+void ModListViewActions::setCategoriesIf(ModInfo::Ptr mod, ModInfo::Ptr ref, const std::vector<std::pair<int, bool>>& categories) const
+{
+ for (auto& [id, enabled] : categories) {
+ if (ref->categorySet(id) != enabled) {
+ mod->setCategory(id, enabled);
+ }
+ }
+}
+
+void ModListViewActions::setCategories(const QModelIndexList& selected, const QModelIndex& ref,
+ const std::vector<std::pair<int, bool>>& categories) const
+{
+ ModInfo::Ptr refMod = ModInfo::getByIndex(ref.data(ModList::IndexRole).toInt());
+ if (selected.size() > 1) {
+ for (auto& idx : selected) {
+ if (idx.row() != ref.row()) {
+ setCategoriesIf(
+ ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()),
+ refMod, categories);
+ }
+ }
+ setCategories(refMod, categories);
+ }
+ else if (!selected.isEmpty()) {
+ // for single mod selections, just do a replace
+ setCategories(refMod, categories);
+ }
+
+ for (auto& idx : selected) {
+ m_core.modList()->notifyChange(idx.data(ModList::IndexRole).toInt());
+ }
+
+ // reset the selection manually - still needed
+ auto viewIndices = m_view->indexModelToView(selected);
+ for (auto& idx : viewIndices) {
+ m_view->selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows);
+ }
+}
+
+void ModListViewActions::setPrimaryCategory(const QModelIndexList& selected, int category, bool force)
+{
+ for (auto& idx : selected) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt());
+ if (force || info->categorySet(category)) {
+ info->setCategory(category, true);
+ info->setPrimaryCategory(category);
+ }
+ }
+
+ // reset the selection manually - still needed
+ auto viewIndices = m_view->indexModelToView(selected);
+ for (auto& idx : viewIndices) {
+ m_view->selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows);
+ }
+}
+
+void ModListViewActions::openExplorer(const QModelIndexList& index) const
+{
+ for (auto& idx : index) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt());
+ if (!info->isForeign()) {
+ shell::Explore(info->absolutePath());
+ }
+ }
+}
+
+void ModListViewActions::restoreBackup(const QModelIndex& index) const
+{
+ QRegExp backupRegEx("(.*)_backup[0-9]*$");
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt());
+ if (backupRegEx.indexIn(modInfo->name()) != -1) {
+ QString regName = backupRegEx.cap(1);
+ QDir modDir(QDir::fromNativeSeparators(m_core.settings().paths().mods()));
+ if (!modDir.exists(regName) ||
+ (QMessageBox::question(m_parent, tr("Overwrite?"),
+ tr("This will replace the existing mod \"%1\". Continue?").arg(regName),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) {
+ if (modDir.exists(regName) && !shellDelete(QStringList(modDir.absoluteFilePath(regName)))) {
+ reportError(tr("failed to remove mod \"%1\"").arg(regName));
+ }
+ else {
+ QString destinationPath = QDir::fromNativeSeparators(m_core.settings().paths().mods()) + "/" + regName;
+ if (!modDir.rename(modInfo->absolutePath(), destinationPath)) {
+ reportError(tr("failed to rename \"%1\" to \"%2\"").arg(modInfo->absolutePath()).arg(destinationPath));
+ }
+ m_core.refresh();
+ m_view->updateModCount();
+ }
+ }
+ }
+}
+
+void ModListViewActions::moveOverwriteContentsTo(const QString& absolutePath) const
+{
+ ModInfo::Ptr overwriteInfo = ModInfo::getOverwrite();
+ bool successful = shellMove((QDir::toNativeSeparators(overwriteInfo->absolutePath()) + "\\*"),
+ (QDir::toNativeSeparators(absolutePath)), false, m_parent);
+
+ if (successful) {
+ MessageDialog::showMessage(tr("Move successful."), m_parent);
+ }
+ else {
+ const auto e = GetLastError();
+ log::error("Move operation failed: {}", formatSystemMessage(e));
+ }
+
+ m_core.refresh();
+}
+
+void ModListViewActions::createModFromOverwrite() const
+{
+ GuessedValue<QString> name;
+ name.setFilter(&fixDirectoryName);
+
+ while (name->isEmpty()) {
+ bool ok;
+ name.update(QInputDialog::getText(m_parent, tr("Create Mod..."),
+ tr("This will move all files from overwrite into a new, regular mod.\n"
+ "Please enter a name:"), QLineEdit::Normal, "", &ok),
+ GUESS_USER);
+ if (!ok) {
+ return;
+ }
+ }
+
+ if (m_core.modList()->getMod(name) != nullptr) {
+ reportError(tr("A mod with this name already exists"));
+ return;
+ }
+
+ const IModInterface* newMod = m_core.createMod(name);
+ if (newMod == nullptr) {
+ return;
+ }
+
+ moveOverwriteContentsTo(newMod->absolutePath());
+}
+
+void ModListViewActions::moveOverwriteContentToExistingMod() const
+{
+ QStringList mods;
+ auto indexesByPriority = m_core.currentProfile()->getAllIndexesByPriority();
+ for (auto& iter : indexesByPriority) {
+ if ((iter.second != UINT_MAX)) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(iter.second);
+ if (!modInfo->isSeparator() && !modInfo->isForeign() && !modInfo->isOverwrite()) {
+ mods << modInfo->name();
+ }
+ }
+ }
+
+ ListDialog dialog(m_parent);
+ dialog.setWindowTitle("Select a mod...");
+ dialog.setChoices(mods);
+
+ if (dialog.exec() == QDialog::Accepted) {
+ QString result = dialog.getChoice();
+ if (!result.isEmpty()) {
+
+ QString modAbsolutePath;
+
+ for (const auto& mod : m_core.modsSortedByProfilePriority(m_core.currentProfile())) {
+ if (result.compare(mod) == 0) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(mod));
+ modAbsolutePath = modInfo->absolutePath();
+ break;
+ }
+ }
+
+ if (modAbsolutePath.isNull()) {
+ log::warn("Mod {} has not been found, for some reason", result);
+ return;
+ }
+
+ moveOverwriteContentsTo(modAbsolutePath);
+ }
+ }
+}
+
+void ModListViewActions::clearOverwrite() const
+{
+ ModInfo::Ptr modInfo = ModInfo::getOverwrite();
+ if (modInfo)
+ {
+ QDir overwriteDir(modInfo->absolutePath());
+ if (QMessageBox::question(m_parent, tr("Are you sure?"),
+ tr("About to recursively delete:\n") + overwriteDir.absolutePath(),
+ QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok)
+ {
+ QStringList delList;
+ for (auto f : overwriteDir.entryList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot))
+ delList.push_back(overwriteDir.absoluteFilePath(f));
+ if (shellDelete(delList, true)) {
+ emit overwriteCleared();
+ m_core.refresh();
+ }
+ else {
+ const auto e = GetLastError();
+ log::error("Delete operation failed: {}", formatSystemMessage(e));
+ }
+ }
+ }
+}