diff options
| author | Silarn <jrim@rimpo.org> | 2019-12-23 15:58:21 -0600 |
|---|---|---|
| committer | Jeremy Rimpo <jeremy.rimpo@servermonkey.com> | 2023-09-21 16:54:49 -0500 |
| commit | 4fd45b937c0577a0c5c1699726e8132b97be8f5d (patch) | |
| tree | a580bc33cab036a313c9edb6e48c888895e0d4ce /src | |
| parent | 0b6afd6a9a39c60b5420e40189bc0ac60a4766ba (diff) | |
WIP: Initial changes to fetch nexus categories
Diffstat (limited to 'src')
| -rw-r--r-- | src/categories.cpp | 208 | ||||
| -rw-r--r-- | src/categories.h | 45 | ||||
| -rw-r--r-- | src/categoriesdialog.cpp | 15 | ||||
| -rw-r--r-- | src/filterlist.cpp | 25 | ||||
| -rw-r--r-- | src/filterlist.h | 6 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 1175 | ||||
| -rw-r--r-- | src/mainwindow.h | 4 |
7 files changed, 1340 insertions, 138 deletions
diff --git a/src/categories.cpp b/src/categories.cpp index 70efd706..e64d35c3 100644 --- a/src/categories.cpp +++ b/src/categories.cpp @@ -29,6 +29,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QList> #include <QObject> +#include "nexusinterface.h" + using namespace MOBase; CategoryFactory* CategoryFactory::s_Instance = nullptr; @@ -38,6 +40,13 @@ QString CategoryFactory::categoriesFilePath() return qApp->property("dataPath").toString() + "/categories.dat"; } + +QString CategoryFactory::nexusMappingFilePath() +{ + return qApp->property("dataPath").toString() + "/nexuscatmap.dat"; +} + + CategoryFactory::CategoryFactory() { atexit(&cleanup); @@ -48,20 +57,18 @@ void CategoryFactory::loadCategories() reset(); QFile categoryFile(categoriesFilePath()); + bool needLoad = false; if (!categoryFile.open(QIODevice::ReadOnly)) { - loadDefaultCategories(); + needLoad = true; } else { int lineNum = 0; while (!categoryFile.atEnd()) { QByteArray line = categoryFile.readLine(); ++lineNum; QList<QByteArray> cells = line.split('|'); - if (cells.count() != 4) { - log::error("invalid category line {}: {} ({} cells)", lineNum, line.constData(), - cells.count()); - } else { - std::vector<int> nexusIDs; + if (cells.count() == 4) { + std::vector<NexusCategory> nexusCats; if (cells[2].length() > 0) { QList<QByteArray> nexusIDStrings = cells[2].split(','); for (QList<QByteArray>::iterator iter = nexusIDStrings.begin(); @@ -69,9 +76,9 @@ void CategoryFactory::loadCategories() bool ok = false; int temp = iter->toInt(&ok); if (!ok) { - log::error("invalid category id {}", iter->constData()); + log::error(tr("invalid category id {}"), iter->constData()); } - nexusIDs.push_back(temp); + nexusCats.push_back(NexusCategory("Unknown", temp)); } } bool cell0Ok = true; @@ -79,23 +86,63 @@ void CategoryFactory::loadCategories() int id = cells[0].toInt(&cell0Ok); int parentID = cells[3].trimmed().toInt(&cell3Ok); if (!cell0Ok || !cell3Ok) { - log::error("invalid category line {}: {}", lineNum, line.constData()); + log::error(tr("invalid category line {}: {}"), lineNum, line.constData()); } - addCategory(id, QString::fromUtf8(cells[1].constData()), nexusIDs, parentID); + addCategory(id, QString::fromUtf8(cells[1].constData()), nexusCats, parentID); + } else if (cells.count() == 3) { + bool cell0Ok = true; + bool cell3Ok = true; + int id = cells[0].toInt(&cell0Ok); + int parentID = cells[2].trimmed().toInt(&cell3Ok); + if (!cell0Ok || !cell3Ok) { + log::error(tr("invalid category line {}: {}"), lineNum, line.constData()); + } + + addCategory(id, QString::fromUtf8(cells[1].constData()), std::vector<NexusCategory>(), parentID); + } else { + log::error( + tr("invalid category line {}: {} ({} cells)"), + lineNum, line.constData(), cells.count()); } } categoryFile.close(); + + QFile nexusMapFile(nexusMappingFilePath()); + if (!nexusMapFile.open(QIODevice::ReadOnly)) { + needLoad = true; + } else { + int nexLineNum = 0; + while (!nexusMapFile.atEnd()) { + QByteArray nexLine = nexusMapFile.readLine(); + ++nexLineNum; + QList<QByteArray> nexCells = nexLine.split('|'); + std::vector<NexusCategory> nexusCats; + QString nexName = nexCells[1]; + bool ok = false; + int nexID = nexCells[2].toInt(&ok); + if (!ok) { + log::error(tr("invalid nexus ID {}"), nexCells[2].constData()); + } + int catID = nexCells[0].toInt(&ok); + if (!ok) { + log::error(tr("invalid category id {}"), nexCells[0].constData()); + } + m_NexusMap[NexusCategory(nexName, nexID)] = catID; + } + } + nexusMapFile.close(); } std::sort(m_Categories.begin(), m_Categories.end()); setParents(); + if (needLoad) loadDefaultCategories(); } -CategoryFactory& CategoryFactory::instance() +CategoryFactory* CategoryFactory::instance() { if (s_Instance == nullptr) { s_Instance = new CategoryFactory; } - return *s_Instance; + return s_Instance; } void CategoryFactory::reset() @@ -106,7 +153,7 @@ void CategoryFactory::reset() // 43 = Savegames (makes no sense to install them through MO) // 45 = Videos and trailers // 87 = Miscelanous - addCategory(0, "None", {4, 28, 43, 45, 87}, 0); + addCategory(0, "None", std::vector<NexusCategory>(), 0); } void CategoryFactory::setParents() @@ -139,7 +186,7 @@ void CategoryFactory::saveCategories() QFile categoryFile(categoriesFilePath()); if (!categoryFile.open(QIODevice::WriteOnly)) { - reportError(QObject::tr("Failed to save custom categories")); + reportError(tr("Failed to save custom categories")); return; } @@ -154,13 +201,28 @@ void CategoryFactory::saveCategories() .append("|") .append(iter->m_Name.toUtf8()) .append("|") - .append(VectorJoin(iter->m_NexusIDs, ",").toUtf8()) - .append("|") .append(QByteArray::number(iter->m_ParentID)) .append("\n"); categoryFile.write(line); } categoryFile.close(); + + QFile nexusMapFile(nexusMappingFilePath()); + + if (!nexusMapFile.open(QIODevice::WriteOnly)) { + reportError(tr("Failed to save nexus category mappings")); + return; + } + + nexusMapFile.resize(0); + QByteArray line; + for (auto iter = m_NexusMap.begin(); iter != m_NexusMap.end(); ++iter) { + line.append(iter->first.m_Name).append("|"); + line.append(iter->first.m_ID).append("|"); + line.append(iter->second).append("\n"); + categoryFile.write(line); + } + categoryFile.close(); } unsigned int @@ -175,97 +237,55 @@ CategoryFactory::countCategories(std::function<bool(const Category& category)> f return result; } -int CategoryFactory::addCategory(const QString& name, const std::vector<int>& nexusIDs, - int parentID) +int CategoryFactory::addCategory(const QString& name, const std::vector<NexusCategory>& nexusCats, int parentID) { int id = 1; while (m_IDMap.find(id) != m_IDMap.end()) { ++id; } - addCategory(id, name, nexusIDs, parentID); + addCategory(id, name, nexusCats, parentID); saveCategories(); return id; } -void CategoryFactory::addCategory(int id, const QString& name, - const std::vector<int>& nexusIDs, int parentID) +void CategoryFactory::addCategory(int id, const QString& name, int parentID) { int index = static_cast<int>(m_Categories.size()); - m_Categories.push_back(Category(index, id, name, nexusIDs, parentID)); - for (int nexusID : nexusIDs) { - m_NexusMap[nexusID] = index; - } + m_Categories.push_back(Category(index, id, name, parentID)); m_IDMap[id] = index; } +void CategoryFactory::addCategory(int id, const QString& name, const std::vector<NexusCategory>& nexusCats, int parentID) +{ + for (auto nexusCat : nexusCats) { + m_NexusMap[nexusCat] = id; + } + addCategory(id, name, parentID); +} + + void CategoryFactory::loadDefaultCategories() { // the order here is relevant as it defines the order in which the // mods appear in the combo box - addCategory(1, "Animations", {2, 4, 51}, 0); - addCategory(52, "Poses", {1, 29}, 1); - addCategory(2, "Armour", {2, 5, 54}, 0); - addCategory(53, "Power Armor", {1, 53}, 2); - addCategory(3, "Audio", {3, 33, 35, 106}, 0); - addCategory(38, "Music", {2, 34, 61}, 0); - addCategory(39, "Voice", {2, 36, 107}, 0); - addCategory(5, "Clothing", {2, 9, 60}, 0); - addCategory(41, "Jewelry", {1, 102}, 5); - addCategory(42, "Backpacks", {1, 49}, 5); - addCategory(6, "Collectables", {2, 10, 92}, 0); - addCategory(28, "Companions", {3, 11, 66, 96}, 0); - addCategory(7, "Creatures, Mounts, & Vehicles", {4, 12, 65, 83, 101}, 0); - addCategory(8, "Factions", {2, 16, 25}, 0); - addCategory(9, "Gameplay", {2, 15, 24}, 0); - addCategory(27, "Combat", {1, 77}, 9); - addCategory(43, "Crafting", {2, 50, 100}, 9); - addCategory(48, "Overhauls", {2, 24, 79}, 9); - addCategory(49, "Perks", {1, 27}, 9); - addCategory(54, "Radio", {1, 31}, 9); - addCategory(55, "Shouts", {1, 104}, 9); - addCategory(22, "Skills & Levelling", {2, 46, 73}, 9); - addCategory(58, "Weather & Lighting", {1, 56}, 9); - addCategory(44, "Equipment", {1, 44}, 43); - addCategory(45, "Home/Settlement", {1, 45}, 43); - addCategory(10, "Body, Face, & Hair", {2, 17, 26}, 0); - addCategory(56, "Tattoos", {1, 57}, 10); - addCategory(40, "Character Presets", {1, 58}, 0); - addCategory(11, "Items", {2, 27, 85}, 0); - addCategory(32, "Mercantile", {2, 23, 69}, 0); - addCategory(37, "Ammo", {1, 3}, 11); - addCategory(19, "Weapons", {2, 41, 55}, 11); - addCategory(36, "Weapon & Armour Sets", {1, 42}, 11); - addCategory(23, "Player Homes", {2, 28, 67}, 0); - addCategory(25, "Castles & Mansions", {1, 68}, 23); - addCategory(51, "Settlements", {1, 48}, 23); - addCategory(12, "Locations", {10, 20, 21, 22, 30, 47, 70, 88, 89, 90, 91}, 0); - addCategory(4, "Cities", {1, 53}, 12); - addCategory(31, "Landscape Changes", {1, 58}, 0); - addCategory(29, "Environment", {2, 14, 74}, 0); - addCategory(30, "Immersion", {2, 51, 78}, 0); - addCategory(20, "Magic", {3, 75, 93, 94}, 0); - addCategory(21, "Models & Textures", {2, 19, 29}, 0); - addCategory(33, "Modders resources", {2, 18, 82}, 0); - addCategory(13, "NPCs", {3, 22, 33, 99}, 0); - addCategory(24, "Bugfixes", {2, 6, 95}, 0); - addCategory(14, "Patches", {2, 25, 84}, 24); - addCategory(35, "Utilities", {2, 38, 39}, 0); - addCategory(26, "Cheats", {1, 8}, 0); - addCategory(15, "Quests", {2, 30, 35}, 0); - addCategory(16, "Races & Classes", {1, 34}, 0); - addCategory(34, "Stealth", {1, 76}, 0); - addCategory(17, "UI", {2, 37, 42}, 0); - addCategory(18, "Visuals", {2, 40, 62}, 0); - addCategory(50, "Pip-Boy", {1, 52}, 18); - addCategory(46, "Shader Presets", {3, 13, 97, 105}, 0); - addCategory(47, "Miscellaneous", {2, 2, 28}, 0); + if (QMessageBox::question(nullptr, tr("Load Nexus Categories?"), + tr("This is either a new or old instance which lacks modern Nexus category mappings. Would you like to import and map categories from Nexus now?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + emit requestNexusCategories(); + } +} + + +void CategoryFactory::mapNexusCategories(QString, QVariant, QVariant result) +{ + } int CategoryFactory::getParentID(unsigned int index) const { if (index >= m_Categories.size()) { - throw MyException(QObject::tr("invalid category index: %1").arg(index)); + throw MyException(tr("invalid category index: %1").arg(index)); } return m_Categories[index].m_ParentID; @@ -303,7 +323,7 @@ bool CategoryFactory::isDescendantOfImpl(int id, int parentID, return isDescendantOfImpl(m_Categories[index].m_ParentID, parentID, seen); } } else { - log::warn("{} is no valid category id", id); + log::warn(tr("{} is no valid category id"), id); return false; } } @@ -311,7 +331,7 @@ bool CategoryFactory::isDescendantOfImpl(int id, int parentID, bool CategoryFactory::hasChildren(unsigned int index) const { if (index >= m_Categories.size()) { - throw MyException(QObject::tr("invalid category index: %1").arg(index)); + throw MyException(tr("invalid category index: %1").arg(index)); } return m_Categories[index].m_HasChildren; @@ -320,7 +340,7 @@ bool CategoryFactory::hasChildren(unsigned int index) const QString CategoryFactory::getCategoryName(unsigned int index) const { if (index >= m_Categories.size()) { - throw MyException(QObject::tr("invalid category index: %1").arg(index)); + throw MyException(tr("invalid category index: %1").arg(index)); } return m_Categories[index].m_Name; @@ -388,7 +408,7 @@ QString CategoryFactory::getCategoryNameByID(int id) const int CategoryFactory::getCategoryID(unsigned int index) const { if (index >= m_Categories.size()) { - throw MyException(QObject::tr("invalid category index: %1").arg(index)); + throw MyException(tr("invalid category index: %1").arg(index)); } return m_Categories[index].m_ID; @@ -398,7 +418,7 @@ int CategoryFactory::getCategoryIndex(int ID) const { std::map<int, unsigned int>::const_iterator iter = m_IDMap.find(ID); if (iter == m_IDMap.end()) { - throw MyException(QObject::tr("invalid category id: %1").arg(ID)); + throw MyException(tr("invalid category id: %1").arg(ID)); } return iter->second; } @@ -419,12 +439,14 @@ int CategoryFactory::getCategoryID(const QString& name) const unsigned int CategoryFactory::resolveNexusID(int nexusID) const { - std::map<int, unsigned int>::const_iterator iter = m_NexusMap.find(nexusID); - if (iter != m_NexusMap.end()) { - log::debug("nexus category id {} maps to internal {}", nexusID, iter->second); - return iter->second; + auto result = std::find_if(m_NexusMap.begin(), m_NexusMap.end(), [nexusID](const std::pair<NexusCategory, unsigned int> el) { + return el.first.m_ID == nexusID; + }); + if (result != m_NexusMap.end()) { + log::debug(tr("nexus category id {} maps to internal {}"), nexusID, result->second); + return result->second; } else { - log::debug("nexus category id {} not mapped", nexusID); + log::debug(tr("nexus category id {} not mapped"), nexusID); return 0U; } } diff --git a/src/categories.h b/src/categories.h index b938bd19..232288a7 100644 --- a/src/categories.h +++ b/src/categories.h @@ -31,8 +31,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. *to look up categories, optimized to where the request comes from. Therefore be very *careful which of the two you have available **/ -class CategoryFactory -{ +class CategoryFactory : QObject { + Q_OBJECT; friend class CategoriesDialog; @@ -53,19 +53,14 @@ public: }; public: - struct Category - { - Category(int sortValue, int id, const QString& name, - const std::vector<int>& nexusIDs, int parentID) - : m_SortValue(sortValue), m_ID(id), m_Name(name), m_HasChildren(false), - m_NexusIDs(nexusIDs), m_ParentID(parentID) - {} + struct Category { + Category(int sortValue, int id, const QString& name, int parentID) + : m_SortValue(sortValue), m_ID(id), m_Name(name), m_HasChildren(false), m_ParentID(parentID) {} int m_SortValue; int m_ID; int m_ParentID; bool m_HasChildren; QString m_Name; - std::vector<int> m_NexusIDs; friend bool operator<(const Category& LHS, const Category& RHS) { @@ -73,6 +68,13 @@ public: } }; + struct NexusCategory { + NexusCategory(const QString &name, const int nexusID) + : m_Name(name), m_ID(nexusID) {} + QString m_Name; + int m_ID; + }; + public: /** * @brief reset the list of categories @@ -89,7 +91,7 @@ public: **/ void saveCategories(); - int addCategory(const QString& name, const std::vector<int>& nexusIDs, int parentID); + int addCategory(const QString& name, const std::vector<NexusCategory>& nexusCats, int parentID); /** * @brief retrieve the number of available categories @@ -183,20 +185,33 @@ public: * * @return the reference to the singleton **/ - static CategoryFactory& instance(); + static CategoryFactory* instance(); /** * @return path to the file that contains the categories list */ static QString categoriesFilePath(); + /** + * @return path to the file that contains the nexus category mappings + */ + static QString nexusMappingFilePath(); + +public slots: + + void mapNexusCategories(QString, QVariant, QVariant data); + +signals: + + void requestNexusCategories(); + private: CategoryFactory(); void loadDefaultCategories(); - void addCategory(int id, const QString& name, const std::vector<int>& nexusID, - int parentID); + void addCategory(int id, const QString& name, const std::vector<NexusCategory>& nexusCats, int parentID); + void addCategory(int id, const QString& name, int parentID); void setParents(); @@ -207,7 +222,7 @@ private: std::vector<Category> m_Categories; std::map<int, unsigned int> m_IDMap; - std::map<int, unsigned int> m_NexusMap; + std::map<NexusCategory, unsigned int> m_NexusMap; private: // called by isDescendantOf() diff --git a/src/categoriesdialog.cpp b/src/categoriesdialog.cpp index 99a931ef..70759419 100644 --- a/src/categoriesdialog.cpp +++ b/src/categoriesdialog.cpp @@ -132,8 +132,8 @@ void CategoriesDialog::cellChanged(int row, int) void CategoriesDialog::commitChanges() { - CategoryFactory& categories = CategoryFactory::instance(); - categories.reset(); + CategoryFactory* categories = CategoryFactory::instance(); + categories->reset(); for (int i = 0; i < ui->categoriesTable->rowCount(); ++i) { int index = ui->categoriesTable->verticalHeader()->logicalIndex(i); @@ -145,13 +145,14 @@ void CategoriesDialog::commitChanges() nexusIDs.push_back(iter->toInt()); } - categories.addCategory(ui->categoriesTable->item(index, 0)->text().toInt(), - ui->categoriesTable->item(index, 1)->text(), nexusIDs, - ui->categoriesTable->item(index, 3)->text().toInt()); + categories->addCategory( + ui->categoriesTable->item(index, 0)->text().toInt(), + ui->categoriesTable->item(index, 1)->text(), nexusIDs, + ui->categoriesTable->item(index, 3)->text().toInt()); } - categories.setParents(); + categories->setParents(); - categories.saveCategories(); + categories->saveCategories(); } void CategoriesDialog::refreshIDs() diff --git a/src/filterlist.cpp b/src/filterlist.cpp index d0c7199e..54853227 100644 --- a/src/filterlist.cpp +++ b/src/filterlist.cpp @@ -187,9 +187,9 @@ private: } }; -FilterList::FilterList(Ui::MainWindow* ui, OrganizerCore& core, - CategoryFactory& factory) - : ui(ui), m_core(core), m_factory(factory) + +FilterList::FilterList(Ui::MainWindow* ui, OrganizerCore* organizer, CategoryFactory* factory) + : ui(ui), m_Organizer(organizer), m_factory(factory) { auto* eventFilter = new CriteriaItemFilter(ui->filters, [&](auto* item, int dir) { return cycleItem(item, dir); @@ -274,15 +274,15 @@ void FilterList::addContentCriteria() void FilterList::addCategoryCriteria(QTreeWidgetItem* root, const std::set<int>& categoriesUsed, int targetID) { - const auto count = static_cast<unsigned int>(m_factory.numCategories()); + const auto count = static_cast<unsigned int>(m_factory->numCategories()); for (unsigned int i = 1; i < count; ++i) { - if (m_factory.getParentID(i) == targetID) { - int categoryID = m_factory.getCategoryID(i); + if (m_factory->getParentID(i) == targetID) { + int categoryID = m_factory->getCategoryID(i); if (categoriesUsed.find(categoryID) != categoriesUsed.end()) { QTreeWidgetItem* item = - addCriteriaItem(root, m_factory.getCategoryName(i), categoryID, - ModListSortProxy::TypeCategory); - if (m_factory.hasChildren(i)) { + addCriteriaItem(root, m_factory->getCategoryName(i), + categoryID, ModListSortProxy::TypeCategory); + if (m_factory->hasChildren(i)) { addCategoryCriteria(item, categoriesUsed, categoryID); } } @@ -294,8 +294,9 @@ void FilterList::addSpecialCriteria(int type) { const auto sc = static_cast<CategoryFactory::SpecialCategories>(type); - addCriteriaItem(nullptr, m_factory.getSpecialCategoryName(sc), type, - ModListSortProxy::TypeSpecial); + addCriteriaItem( + nullptr, m_factory->getSpecialCategoryName(sc), + type, ModListSortProxy::TypeSpecial); } void FilterList::refresh() @@ -332,7 +333,7 @@ void FilterList::refresh() log::warn("cycle in categories: {}", SetJoin(cycleTest, ", ")); break; } - currentID = m_factory.getParentID(m_factory.getCategoryIndex(currentID)); + currentID = m_factory->getParentID(m_factory->getCategoryIndex(currentID)); } } } diff --git a/src/filterlist.h b/src/filterlist.h index f572762e..7bcbdc0f 100644 --- a/src/filterlist.h +++ b/src/filterlist.h @@ -17,7 +17,7 @@ class FilterList : public QObject Q_OBJECT; public: - FilterList(Ui::MainWindow* ui, OrganizerCore& organizer, CategoryFactory& factory); + FilterList(Ui::MainWindow* ui, OrganizerCore* organizer, CategoryFactory* factory); void restoreState(const Settings& s); void saveState(Settings& s) const; @@ -35,8 +35,8 @@ private: class CriteriaItem; Ui::MainWindow* ui; - OrganizerCore& m_core; - CategoryFactory& m_factory; + OrganizerCore* m_Organizer; + CategoryFactory* m_factory; bool onClick(QMouseEvent* e); void onItemActivated(QTreeWidgetItem* item); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ebe7da98..37113954 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -290,6 +290,19 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, ui->statusBar->setAPI(ni.getAPIStats(), ni.getAPIUserAccount()); } + languageChange(settings.interface().language()); + + m_CategoryFactory->loadCategories(); + m_Filters.reset(new FilterList(ui, &m_OrganizerCore, m_CategoryFactory)); + + connect( + m_Filters.get(), &FilterList::criteriaChanged, + [&](auto&& v) { onFiltersCriteria(v); }); + + connect( + m_Filters.get(), &FilterList::optionsChanged, + [&](auto&& mode, auto&& sep) { onFiltersOptions(mode, sep); }); + ui->logList->setCore(m_OrganizerCore); setupToolbar(); @@ -406,9 +419,13 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, connect(&NexusInterface::instance(), SIGNAL(needLogin()), &m_OrganizerCore, SLOT(nexusApi())); - connect(NexusInterface::instance().getAccessManager(), - SIGNAL(credentialsReceived(const APIUserAccount&)), this, - SLOT(updateWindowTitle(const APIUserAccount&))); + connect(m_CategoryFactory, SIGNAL(requestNexusCategories()), &m_OrganizerCore, SLOT(requestNexusCategories())); + + connect( + NexusInterface::instance(&pluginContainer)->getAccessManager(), + SIGNAL(credentialsReceived(const APIUserAccount&)), + this, + SLOT(updateWindowTitle(const APIUserAccount&))); connect(NexusInterface::instance().getAccessManager(), SIGNAL(credentialsReceived(const APIUserAccount&)), @@ -2009,9 +2026,9 @@ void MainWindow::fixCategories() for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { ModInfo::Ptr modInfo = ModInfo::getByIndex(i); std::set<int> categories = modInfo->getCategories(); - for (std::set<int>::iterator iter = categories.begin(); iter != categories.end(); - ++iter) { - if (!m_CategoryFactory.categoryExists(*iter)) { + for (std::set<int>::iterator iter = categories.begin(); + iter != categories.end(); ++iter) { + if (!m_CategoryFactory->categoryExists(*iter)) { modInfo->setCategory(*iter, false); } } @@ -2471,6 +2488,738 @@ void MainWindow::refreshProfile_activated() m_OrganizerCore.profileRefresh(); } +void MainWindow::updateModCount() +{ + int activeCount = 0; + int visActiveCount = 0; + int backupCount = 0; + int visBackupCount = 0; + int foreignCount = 0; + int visForeignCount = 0; + int separatorCount = 0; + int visSeparatorCount = 0; + int regularCount = 0; + int visRegularCount = 0; + + QStringList allMods = m_OrganizerCore.modList()->allMods(); + + auto hasFlag = [](std::vector<ModInfo::EFlag> flags, ModInfo::EFlag filter) { + return std::find(flags.begin(), flags.end(), filter) != flags.end(); + }; + + bool isEnabled; + bool isVisible; + for (QString mod : allMods) { + int modIndex = ModInfo::getIndex(mod); + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + std::vector<ModInfo::EFlag> modFlags = modInfo->getFlags(); + isEnabled = m_OrganizerCore.currentProfile()->modEnabled(modIndex); + isVisible = m_ModListSortProxy->filterMatchesMod(modInfo, isEnabled); + + for (auto flag : modFlags) { + switch (flag) { + case ModInfo::FLAG_BACKUP: backupCount++; + if (isVisible) + visBackupCount++; + break; + case ModInfo::FLAG_FOREIGN: foreignCount++; + if (isVisible) + visForeignCount++; + break; + case ModInfo::FLAG_SEPARATOR: separatorCount++; + if (isVisible) + visSeparatorCount++; + break; + } + } + + if (!hasFlag(modFlags, ModInfo::FLAG_BACKUP) && + !hasFlag(modFlags, ModInfo::FLAG_FOREIGN) && + !hasFlag(modFlags, ModInfo::FLAG_SEPARATOR) && + !hasFlag(modFlags, ModInfo::FLAG_OVERWRITE)) { + if (isEnabled) { + activeCount++; + if (isVisible) + visActiveCount++; + } + if (isVisible) + visRegularCount++; + regularCount++; + } + } + + ui->activeModsCounter->display(visActiveCount); + ui->activeModsCounter->setToolTip(tr("<table cellspacing=\"5\">" + "<tr><th>Type</th><th>All</th><th>Visible</th>" + "<tr><td>Enabled mods: </td><td align=right>%1 / %2</td><td align=right>%3 / %4</td></tr>" + "<tr><td>Unmanaged/DLCs: </td><td align=right>%5</td><td align=right>%6</td></tr>" + "<tr><td>Mod backups: </td><td align=right>%7</td><td align=right>%8</td></tr>" + "<tr><td>Separators: </td><td align=right>%9</td><td align=right>%10</td></tr>" + "</table>") + .arg(activeCount) + .arg(regularCount) + .arg(visActiveCount) + .arg(visRegularCount) + .arg(foreignCount) + .arg(visForeignCount) + .arg(backupCount) + .arg(visBackupCount) + .arg(separatorCount) + .arg(visSeparatorCount) + ); +} + +void MainWindow::updatePluginCount() +{ + int activeMasterCount = 0; + int activeLightMasterCount = 0; + int activeRegularCount = 0; + int masterCount = 0; + int lightMasterCount = 0; + int regularCount = 0; + int activeVisibleCount = 0; + + PluginList *list = m_OrganizerCore.pluginList(); + QString filter = ui->espFilterEdit->text(); + + for (QString plugin : list->pluginNames()) { + bool active = list->isEnabled(plugin); + bool visible = m_PluginListSortProxy->filterMatchesPlugin(plugin); + if (list->isLight(plugin) || list->isLightFlagged(plugin)) { + lightMasterCount++; + activeLightMasterCount += active; + activeVisibleCount += visible && active; + } else if (list->isMaster(plugin)) { + masterCount++; + activeMasterCount += active; + activeVisibleCount += visible && active; + } else { + regularCount++; + activeRegularCount += active; + activeVisibleCount += visible && active; + } + } + + int activeCount = activeMasterCount + activeLightMasterCount + activeRegularCount; + int totalCount = masterCount + lightMasterCount + regularCount; + + ui->activePluginsCounter->display(activeVisibleCount); + ui->activePluginsCounter->setToolTip(tr("<table cellspacing=\"6\">" + "<tr><th>Type</th><th>Active </th><th>Total</th></tr>" + "<tr><td>All plugins:</td><td align=right>%1 </td><td align=right>%2</td></tr>" + "<tr><td>ESMs:</td><td align=right>%3 </td><td align=right>%4</td></tr>" + "<tr><td>ESPs:</td><td align=right>%7 </td><td align=right>%8</td></tr>" + "<tr><td>ESMs+ESPs:</td><td align=right>%9 </td><td align=right>%10</td></tr>" + "<tr><td>ESLs:</td><td align=right>%5 </td><td align=right>%6</td></tr>" + "</table>") + .arg(activeCount).arg(totalCount) + .arg(activeMasterCount).arg(masterCount) + .arg(activeLightMasterCount).arg(lightMasterCount) + .arg(activeRegularCount).arg(regularCount) + .arg(activeMasterCount+activeRegularCount).arg(masterCount+regularCount) + ); +} + +void MainWindow::information_clicked() +{ + try { + displayModInformation(m_ContextRow); + } catch (const std::exception &e) { + reportError(e.what()); + } +} + +void MainWindow::createEmptyMod_clicked() +{ + GuessedValue<QString> name; + name.setFilter(&fixDirectoryName); + + while (name->isEmpty()) { + bool ok; + name.update(QInputDialog::getText(this, 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_OrganizerCore.getMod(name) != nullptr) { + reportError(tr("A mod with this name already exists")); + return; + } + + int newPriority = -1; + if (m_ContextRow >= 0 && m_ModListSortProxy->sortColumn() == ModList::COL_PRIORITY) { + newPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow); + } + + IModInterface *newMod = m_OrganizerCore.createMod(name); + if (newMod == nullptr) { + return; + } + + m_OrganizerCore.refreshModList(); + + if (newPriority >= 0) { + m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority); + } +} + +void MainWindow::createSeparator_clicked() +{ + GuessedValue<QString> name; + name.setFilter(&fixDirectoryName); + while (name->isEmpty()) + { + bool ok; + name.update(QInputDialog::getText(this, 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_OrganizerCore.getMod(name) != nullptr) + { + reportError(tr("A separator with this name already exists")); + return; + } + name->append("_separator"); + if (m_OrganizerCore.getMod(name) != nullptr) + { + return; + } + + int newPriority = -1; + if (m_ContextRow >= 0 && m_ModListSortProxy->sortColumn() == ModList::COL_PRIORITY) + { + newPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow); + } + + if (m_OrganizerCore.createMod(name) == nullptr) { return; } + m_OrganizerCore.refreshModList(); + + if (newPriority >= 0) + { + m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority); + } + + if (auto c=m_OrganizerCore.settings().colors().previousSeparatorColor()) { + ModInfo::getByIndex(ModInfo::getIndex(name))->setColor(*c); + } +} + +void MainWindow::setColor_clicked() +{ + auto& settings = m_OrganizerCore.settings(); + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + + QColorDialog dialog(this); + 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); + + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + info->setColor(currentColor); + } + } + else { + modInfo->setColor(currentColor); + } +} + +void MainWindow::resetColor_clicked() +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + QColor color = QColor(); + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + info->setColor(color); + } + } + else { + modInfo->setColor(color); + } + + m_OrganizerCore.settings().colors().removePreviousSeparatorColor(); +} + +void MainWindow::createModFromOverwrite() +{ + GuessedValue<QString> name; + name.setFilter(&fixDirectoryName); + + while (name->isEmpty()) { + bool ok; + name.update(QInputDialog::getText(this, 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_OrganizerCore.getMod(name) != nullptr) { + reportError(tr("A mod with this name already exists")); + return; + } + + const IModInterface *newMod = m_OrganizerCore.createMod(name); + if (newMod == nullptr) { + return; + } + + doMoveOverwriteContentToMod(newMod->absolutePath()); +} + +void MainWindow::moveOverwriteContentToExistingMod() +{ + QStringList mods; + auto indexesByPriority = m_OrganizerCore.currentProfile()->getAllIndexesByPriority(); + for (auto & iter : indexesByPriority) { + if ((iter.second != UINT_MAX)) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(iter.second); + if (!modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) && !modInfo->hasFlag(ModInfo::FLAG_FOREIGN) && !modInfo->hasFlag(ModInfo::FLAG_OVERWRITE)) { + mods << modInfo->name(); + } + } + } + + ListDialog dialog(this); + 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_OrganizerCore.modsSortedByProfilePriority()) { + 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; + } + + doMoveOverwriteContentToMod(modAbsolutePath); + } + } +} + +void MainWindow::doMoveOverwriteContentToMod(const QString &modAbsolutePath) +{ + unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { + std::vector<ModInfo::EFlag> flags = mod->getFlags(); + return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); }); + + ModInfo::Ptr overwriteInfo = ModInfo::getByIndex(overwriteIndex); + bool successful = shellMove((QDir::toNativeSeparators(overwriteInfo->absolutePath()) + "\\*"), + (QDir::toNativeSeparators(modAbsolutePath)), false, this); + + if (successful) { + MessageDialog::showMessage(tr("Move successful."), this); + } + else { + const auto e = GetLastError(); + log::error("Move operation failed: {}", formatSystemMessage(e)); + } + + m_OrganizerCore.refreshModList(); +} + +void MainWindow::clearOverwrite() +{ + unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { + std::vector<ModInfo::EFlag> flags = mod->getFlags(); + return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) + != flags.end(); + }); + + ModInfo::Ptr modInfo = ModInfo::getByIndex(overwriteIndex); + if (modInfo) + { + QDir overwriteDir(modInfo->absolutePath()); + if (QMessageBox::question(this, 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)) { + scheduleCheckForProblems(); + m_OrganizerCore.refreshModList(); + } else { + const auto e = GetLastError(); + log::error("Delete operation failed: {}", formatSystemMessage(e)); + } + } + } +} + +void MainWindow::cancelModListEditor() +{ + ui->modList->setEnabled(false); + ui->modList->setEnabled(true); +} + +void MainWindow::on_modList_doubleClicked(const QModelIndex &index) +{ + if (!index.isValid()) { + return; + } + + if (m_OrganizerCore.modList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) { + // don't interpret double click if we only just checked a mod + return; + } + + QModelIndex sourceIdx = mapToModel(m_OrganizerCore.modList(), index); + if (!sourceIdx.isValid()) { + return; + } + + Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers(); + if (modifiers.testFlag(Qt::ControlModifier)) { + try { + m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); + + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + shell::Explore(modInfo->absolutePath()); + + // workaround to cancel the editor that might have opened because of + // selection-click + ui->modList->closePersistentEditor(index); + } + catch (const std::exception &e) { + reportError(e.what()); + } + } + else if (modifiers.testFlag(Qt::ShiftModifier)) { + try { + m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); + QModelIndex idx = m_OrganizerCore.modList()->index(m_ContextRow, 0); + visitNexusOrWebPage(idx); + ui->modList->closePersistentEditor(index); + } + catch (const std::exception & e) { + reportError(e.what()); + } + } + else{ + try { + m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); + sourceIdx.column(); + + auto tab = ModInfoTabIDs::None; + + switch (sourceIdx.column()) { + case ModList::COL_NOTES: tab = ModInfoTabIDs::Notes; break; + case ModList::COL_VERSION: tab = ModInfoTabIDs::Nexus; break; + case ModList::COL_MODID: tab = ModInfoTabIDs::Nexus; break; + case ModList::COL_GAME: tab = ModInfoTabIDs::Nexus; break; + case ModList::COL_CATEGORY: tab = ModInfoTabIDs::Categories; break; + case ModList::COL_CONFLICTFLAGS: tab = ModInfoTabIDs::Conflicts; break; + } + + displayModInformation(sourceIdx.row(), tab); + // workaround to cancel the editor that might have opened because of + // selection-click + ui->modList->closePersistentEditor(index); + } + catch (const std::exception &e) { + reportError(e.what()); + } + } +} + +void MainWindow::on_listOptionsBtn_pressed() +{ + m_ContextRow = -1; +} + +void MainWindow::openOriginInformation_clicked() +{ + try { + QItemSelectionModel *selection = ui->espList->selectionModel(); + //we don't want to open multiple modinfodialogs. + /*if (selection->hasSelection() && selection->selectedRows().count() > 0) { + + for (QModelIndex idx : selection->selectedRows()) { + QString fileName = idx.data().toString(); + ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); + + if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { + displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + } + } + } + else {}*/ + QModelIndex idx = selection->currentIndex(); + QString fileName = idx.data().toString(); + + ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); + + if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { + displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + } + } + catch (const std::exception &e) { + reportError(e.what()); + } +} + +void MainWindow::on_espList_doubleClicked(const QModelIndex &index) +{ + if (!index.isValid()) { + return; + } + + if (m_OrganizerCore.pluginList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) { + // don't interpret double click if we only just checked a plugin + return; + } + + QModelIndex sourceIdx = mapToModel(m_OrganizerCore.pluginList(), index); + if (!sourceIdx.isValid()) { + return; + } + try { + + QItemSelectionModel *selection = ui->espList->selectionModel(); + + if (selection->hasSelection() && selection->selectedRows().count() == 1) { + + QModelIndex idx = selection->currentIndex(); + QString fileName = idx.data().toString(); + + if (ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)) == UINT_MAX) + return; + + ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); + + if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { + + Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers(); + if (modifiers.testFlag(Qt::ControlModifier)) { + openExplorer_activated(); + // workaround to cancel the editor that might have opened because of + // selection-click + ui->espList->closePersistentEditor(index); + } + else { + + displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + // workaround to cancel the editor that might have opened because of + // selection-click + ui->espList->closePersistentEditor(index); + } + } + } + } + catch (const std::exception &e) { + reportError(e.what()); + } +} + +bool MainWindow::populateMenuCategories(QMenu *menu, int targetID) +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + const std::set<int> &categories = modInfo->getCategories(); + + bool childEnabled = false; + + for (unsigned int i = 1; i < m_CategoryFactory->numCategories(); ++i) { + if (m_CategoryFactory->getParentID(i) == targetID) { + QMenu *targetMenu = menu; + if (m_CategoryFactory->hasChildren(i)) { + targetMenu = menu->addMenu(m_CategoryFactory->getCategoryName(i).replace('&', "&&")); + } + + int id = m_CategoryFactory->getCategoryID(i); + QScopedPointer<QCheckBox> checkBox(new QCheckBox(targetMenu)); + bool enabled = categories.find(id) != categories.end(); + checkBox->setText(m_CategoryFactory->getCategoryName(i).replace('&', "&&")); + if (enabled) { + childEnabled = true; + } + checkBox->setChecked(enabled ? Qt::Checked : Qt::Unchecked); + + QScopedPointer<QWidgetAction> checkableAction(new QWidgetAction(targetMenu)); + checkableAction->setDefaultWidget(checkBox.take()); + checkableAction->setData(id); + targetMenu->addAction(checkableAction.take()); + + if (m_CategoryFactory->hasChildren(i)) { + if (populateMenuCategories(targetMenu, m_CategoryFactory->getCategoryID(i)) || enabled) { + targetMenu->setIcon(QIcon(":/MO/gui/resources/check.png")); + } + } + } + } + return childEnabled; +} + +void MainWindow::replaceCategoriesFromMenu(QMenu *menu, int modRow) +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(modRow); + for (QAction* action : menu->actions()) { + if (action->menu() != nullptr) { + replaceCategoriesFromMenu(action->menu(), modRow); + } else { + QWidgetAction *widgetAction = qobject_cast<QWidgetAction*>(action); + if (widgetAction != nullptr) { + QCheckBox *checkbox = qobject_cast<QCheckBox*>(widgetAction->defaultWidget()); + modInfo->setCategory(widgetAction->data().toInt(), checkbox->isChecked()); + } + } + } +} + +void MainWindow::addRemoveCategoriesFromMenu(QMenu *menu, int modRow, int referenceRow) +{ + if (referenceRow != -1 && referenceRow != modRow) { + ModInfo::Ptr editedModInfo = ModInfo::getByIndex(referenceRow); + for (QAction* action : menu->actions()) { + if (action->menu() != nullptr) { + addRemoveCategoriesFromMenu(action->menu(), modRow, referenceRow); + } else { + QWidgetAction *widgetAction = qobject_cast<QWidgetAction*>(action); + if (widgetAction != nullptr) { + QCheckBox *checkbox = qobject_cast<QCheckBox*>(widgetAction->defaultWidget()); + int categoryId = widgetAction->data().toInt(); + bool checkedBefore = editedModInfo->categorySet(categoryId); + bool checkedAfter = checkbox->isChecked(); + + if (checkedBefore != checkedAfter) { // only update if the category was changed on the edited mod + ModInfo::Ptr currentModInfo = ModInfo::getByIndex(modRow); + currentModInfo->setCategory(categoryId, checkedAfter); + } + } + } + } + } else { + replaceCategoriesFromMenu(menu, modRow); + } +} + +void MainWindow::addRemoveCategories_MenuHandler() { + QMenu *menu = qobject_cast<QMenu*>(sender()); + if (menu == nullptr) { + log::error("not a menu?"); + return; + } + + QList<QPersistentModelIndex> selected; + for (const QModelIndex &idx : ui->modList->selectionModel()->selectedRows()) { + selected.append(QPersistentModelIndex(idx)); + } + + if (selected.size() > 0) { + int minRow = INT_MAX; + int maxRow = -1; + + for (const QPersistentModelIndex &idx : selected) { + log::debug("change categories on: {}", idx.data().toString()); + QModelIndex modIdx = mapToModel(m_OrganizerCore.modList(), idx); + if (modIdx.row() != m_ContextIdx.row()) { + addRemoveCategoriesFromMenu(menu, modIdx.row(), m_ContextIdx.row()); + } + if (idx.row() < minRow) minRow = idx.row(); + if (idx.row() > maxRow) maxRow = idx.row(); + } + replaceCategoriesFromMenu(menu, m_ContextIdx.row()); + + m_OrganizerCore.modList()->notifyChange(minRow, maxRow + 1); + + for (const QPersistentModelIndex &idx : selected) { + ui->modList->selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows); + } + } else { + //For single mod selections, just do a replace + replaceCategoriesFromMenu(menu, m_ContextRow); + m_OrganizerCore.modList()->notifyChange(m_ContextRow); + } + + refreshFilters(); +} + +void MainWindow::replaceCategories_MenuHandler() { + QMenu *menu = qobject_cast<QMenu*>(sender()); + if (menu == nullptr) { + log::error("not a menu?"); + return; + } + + QList<QPersistentModelIndex> selected; + for (const QModelIndex &idx : ui->modList->selectionModel()->selectedRows()) { + selected.append(QPersistentModelIndex(idx)); + } + + if (selected.size() > 0) { + QStringList selectedMods; + int minRow = INT_MAX; + int maxRow = -1; + for (int i = 0; i < selected.size(); ++i) { + QModelIndex temp = mapToModel(m_OrganizerCore.modList(), selected.at(i)); + selectedMods.append(temp.data().toString()); + replaceCategoriesFromMenu(menu, mapToModel(m_OrganizerCore.modList(), selected.at(i)).row()); + if (temp.row() < minRow) minRow = temp.row(); + if (temp.row() > maxRow) maxRow = temp.row(); + } + + m_OrganizerCore.modList()->notifyChange(minRow, maxRow + 1); + + // find mods by their name because indices are invalidated + QAbstractItemModel *model = ui->modList->model(); + for (const QString &mod : selectedMods) { + QModelIndexList matches = model->match(model->index(0, 0), Qt::DisplayRole, mod, 1, + Qt::MatchFixedString | Qt::MatchCaseSensitive | Qt::MatchRecursive); + if (matches.size() > 0) { + ui->modList->selectionModel()->select(matches.at(0), QItemSelectionModel::Select | QItemSelectionModel::Rows); + } + } + } else { + //For single mod selections, just do a replace + replaceCategoriesFromMenu(menu, m_ContextRow); + m_OrganizerCore.modList()->notifyChange(m_ContextRow); + } + + refreshFilters(); +} + void MainWindow::saveArchiveList() { if (m_OrganizerCore.isArchivesInit()) { @@ -2490,6 +3239,180 @@ void MainWindow::saveArchiveList() } } +void MainWindow::checkModsForUpdates() +{ + bool checkingModsForUpdate = false; + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + checkingModsForUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); + NexusInterface::instance(&m_PluginContainer)->requestEndorsementInfo(this, QVariant(), QString()); + NexusInterface::instance(&m_PluginContainer)->requestTrackingInfo(this, QVariant(), QString()); + } else { + QString apiKey; + if (m_OrganizerCore.settings().nexus().apiKey(apiKey)) { + m_OrganizerCore.doAfterLogin([this] () { this->checkModsForUpdates(); }); + NexusInterface::instance(&m_PluginContainer)->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_OrganizerCore.modList()->allMods()) { + ModInfo::Ptr modInfo = ModInfo::getByName(mod); + if (modInfo->updateAvailable()) { + updatesAvailable = true; + break; + } + } + + if (updatesAvailable || checkingModsForUpdate) { + m_ModListSortProxy->setCriteria({{ + ModListSortProxy::TypeSpecial, + CategoryFactory::UpdateAvailable, + false} + }); + + m_Filters->setSelection({{ + ModListSortProxy::TypeSpecial, + CategoryFactory::UpdateAvailable, + false + }}); + } +} + +void MainWindow::changeVersioningScheme() { + if (QMessageBox::question(this, 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(m_ContextRow); + + 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(this, 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 MainWindow::ignoreUpdate() { + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + info->ignoreUpdate(true); + } + } + else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + info->ignoreUpdate(true); + } + if (m_ModListSortProxy != nullptr) + m_ModListSortProxy->invalidate(); +} + +void MainWindow::checkModUpdates_clicked() +{ + std::multimap<QString, int> IDs; + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + IDs.insert(std::make_pair<QString, int>(info->gameName(), info->nexusId())); + } + } else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + IDs.insert(std::make_pair<QString, int>(info->gameName(), info->nexusId())); + } + modUpdateCheck(IDs); +} + +void MainWindow::unignoreUpdate() +{ + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + info->ignoreUpdate(false); + } + } + else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + info->ignoreUpdate(false); + } + if (m_ModListSortProxy != nullptr) + m_ModListSortProxy->invalidate(); +} + +void MainWindow::addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, + ModInfo::Ptr info) { + const std::set<int> &categories = info->getCategories(); + for (int categoryID : categories) { + int catIdx = m_CategoryFactory->getCategoryIndex(categoryID); + QWidgetAction *action = new QWidgetAction(primaryCategoryMenu); + try { + QRadioButton *categoryBox = new QRadioButton( + m_CategoryFactory->getCategoryName(catIdx).replace('&', "&&"), + primaryCategoryMenu); + connect(categoryBox, &QRadioButton::toggled, [info, categoryID](bool enable) { + if (enable) { + info->setPrimaryCategory(categoryID); + } + }); + categoryBox->setChecked(categoryID == info->primaryCategory()); + action->setDefaultWidget(categoryBox); + } catch (const std::exception &e) { + log::error("failed to create category checkbox: {}", e.what()); + } + + action->setData(categoryID); + primaryCategoryMenu->addAction(action); + } +} + +void MainWindow::addPrimaryCategoryCandidates() +{ + QMenu *menu = qobject_cast<QMenu*>(sender()); + if (menu == nullptr) { + log::error("not a menu?"); + return; + } + menu->clear(); + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + + addPrimaryCategoryCandidates(menu, modInfo); +} + +void MainWindow::enableVisibleMods() +{ + if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really enable all visible mods?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_ModListSortProxy->enableAllVisible(); + } +} + +void MainWindow::disableVisibleMods() +{ + if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really disable all visible mods?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_ModListSortProxy->disableAllVisible(); + } +} + void MainWindow::openInstanceFolder() { QString dataPath = qApp->property("dataPath").toString(); @@ -2549,6 +3472,184 @@ void MainWindow::openMyGamesFolder() shell::Explore(m_OrganizerCore.managedGame()->documentsDirectory()); } + +void MainWindow::exportModListCSV() +{ + //SelectionDialog selection(tr("Choose what to export")); + + //selection.addChoice(tr("Everything"), tr("All installed mods are included in the list"), 0); + //selection.addChoice(tr("Active Mods"), tr("Only active (checked) mods from your current profile are included"), 1); + //selection.addChoice(tr("Visible"), tr("All mods visible in the mod list are included"), 2); + + QDialog selection(this); + 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_OrganizerCore.currentProfile()->getAllIndexesByPriority(); + for (auto& iter : indexesByPriority) { + ModInfo::Ptr info = ModInfo::getByIndex(iter.second); + bool enabled = m_OrganizerCore.currentProfile()->modEnabled(iter.second); + if ((selectedRowID == 1) && !enabled) { + continue; + } + else if ((selectedRowID == 2) && !m_ModListSortProxy->filterMatchesMod(info, enabled)) { + 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_CategoryFactory->categoryExists(info->primaryCategory())) ? m_CategoryFactory->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(&m_PluginContainer)->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(this); + saveDialog.setText(buffer.data()); + saveDialog.exec(); + } + catch (const std::exception &e) { + reportError(tr("export failed: %1").arg(e.what())); + } + } +} + +static void addMenuAsPushButton(QMenu *menu, QMenu *subMenu) +{ + QPushButton *pushBtn = new QPushButton(subMenu->title()); + pushBtn->setMenu(subMenu); + QWidgetAction *action = new QWidgetAction(menu); + action->setDefaultWidget(pushBtn); + menu->addAction(action); +} + QMenu* MainWindow::openFolderMenu() { QMenu* FolderMenu = new QMenu(this); @@ -3538,7 +4639,67 @@ void MainWindow::on_displayCategoriesBtn_toggled(bool checked) setCategoryListVisible(checked); } -void MainWindow::removeFromToolbar(QAction* action) +void MainWindow::deselectFilters() +{ + m_Filters->clearSelection(); +} + +void MainWindow::refreshFilters() +{ + QItemSelection currentSelection = ui->modList->selectionModel()->selection(); + + int idxRow = ui->modList->currentIndex().row(); + QVariant currentIndexName = ui->modList->model()->index(idxRow, 0).data(); + ui->modList->setCurrentIndex(QModelIndex()); + + m_Filters->refresh(); + + ui->modList->selectionModel()->select(currentSelection, QItemSelectionModel::Select); + + QModelIndexList matchList; + if (currentIndexName.isValid()) { + matchList = ui->modList->model()->match( + ui->modList->model()->index(0, 0), + Qt::DisplayRole, + currentIndexName); + } + + if (matchList.size() > 0) { + ui->modList->setCurrentIndex(matchList.at(0)); + } +} + +void MainWindow::onFiltersCriteria(const std::vector<ModListSortProxy::Criteria>& criteria) +{ + m_ModListSortProxy->setCriteria(criteria); + + QString label = "?"; + + if (criteria.empty()) { + label = ""; + } else if (criteria.size() == 1) { + const auto& c = criteria[0]; + + if (c.type == ModListSortProxy::TypeContent) { + const auto *content = m_OrganizerCore.modDataContents().findById(c.id); + label = content ? content->name() : QString(); + } else { + label = m_CategoryFactory->getCategoryNameByID(c.id); + } + + if (label.isEmpty()) { + log::error("category {}:{} not found", c.type, c.id); + } + } else { + label = tr("<Multiple>"); + } + + ui->currentCategoryLabel->setText(label); + ui->modList->reset(); +} + +void MainWindow::onFiltersOptions( + ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep) { const auto& title = action->text(); auto& list = *m_OrganizerCore.executablesList(); diff --git a/src/mainwindow.h b/src/mainwindow.h index 8cd55c90..42ffc83e 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -283,7 +283,9 @@ private: QAction* m_ContextAction; - CategoryFactory& m_CategoryFactory; + QAction* m_browseModPage; + + CategoryFactory* m_CategoryFactory; QTimer m_CheckBSATimer; QTimer m_SaveMetaTimer; |
