diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 500b0ca5b..1b9a0f852 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -42,8 +42,14 @@ mo2_add_filter(NAME src/browser GROUPS browserview ) +mo2_add_filter(NAME src/categories GROUPS + categories + categoriestable + categoriesdialog + categoryimportdialog +) + mo2_add_filter(NAME src/core GROUPS - categories archivefiletree installationmanager nexusinterface @@ -60,7 +66,6 @@ mo2_add_filter(NAME src/core GROUPS mo2_add_filter(NAME src/dialogs GROUPS aboutdialog activatemodsdialog - categoriesdialog credentialsdialog filedialogmemory forcedloaddialog diff --git a/src/categories.cpp b/src/categories.cpp index 70efd7060..c1aa756f3 100644 --- a/src/categories.cpp +++ b/src/categories.cpp @@ -29,6 +29,8 @@ along with Mod Organizer. If not, see . #include #include +#include "nexusinterface.h" + using namespace MOBase; CategoryFactory* CategoryFactory::s_Instance = nullptr; @@ -38,30 +40,33 @@ QString CategoryFactory::categoriesFilePath() return qApp->property("dataPath").toString() + "/categories.dat"; } -CategoryFactory::CategoryFactory() +CategoryFactory::CategoryFactory() : QObject() { atexit(&cleanup); } +QString CategoryFactory::nexusMappingFilePath() +{ + return qApp->property("dataPath").toString() + "/nexuscatmap.dat"; +} + 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 cells = line.split('|'); - if (cells.count() != 4) { - log::error("invalid category line {}: {} ({} cells)", lineNum, line.constData(), - cells.count()); - } else { - std::vector nexusIDs; + if (cells.count() == 4) { + std::vector nexusCats; if (cells[2].length() > 0) { QList nexusIDStrings = cells[2].split(','); for (QList::iterator iter = nexusIDStrings.begin(); @@ -69,9 +74,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 {}").toStdString(), iter->constData()); } - nexusIDs.push_back(temp); + nexusCats.push_back(NexusCategory("Unknown", temp)); } } bool cell0Ok = true; @@ -79,50 +84,94 @@ 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 {}: {}").toStdString(), 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 {}: {}").toStdString(), lineNum, + line.constData()); + } + + addCategory(id, QString::fromUtf8(cells[1].constData()), + std::vector(), parentID); + } else { + log::error(tr("invalid category line {}: {} ({} cells)").toStdString(), 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 nexCells = nexLine.split('|'); + if (nexCells.count() == 3) { + std::vector nexusCats; + QString nexName = nexCells[1]; + bool ok = false; + int nexID = nexCells[2].toInt(&ok); + if (!ok) { + log::error(tr("invalid nexus ID {}").toStdString(), + nexCells[2].constData()); + } + int catID = nexCells[0].toInt(&ok); + if (!ok) { + log::error(tr("invalid category id {}").toStdString(), + nexCells[0].constData()); + } + m_NexusMap.insert_or_assign(nexID, NexusCategory(nexName, nexID)); + m_NexusMap.at(nexID).setCategoryID(catID); + } else { + log::error(tr("invalid nexus category line {}: {} ({} cells)").toStdString(), + lineNum, nexLine.constData(), nexCells.count()); + } + } + } + nexusMapFile.close(); } std::sort(m_Categories.begin(), m_Categories.end()); setParents(); + if (needLoad) + loadDefaultCategories(); } CategoryFactory& CategoryFactory::instance() { - if (s_Instance == nullptr) { - s_Instance = new CategoryFactory; - } - return *s_Instance; + static CategoryFactory s_Instance; + return s_Instance; } void CategoryFactory::reset() { m_Categories.clear(); + m_NexusMap.clear(); m_IDMap.clear(); - // 28 = - // 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(), 0); } void CategoryFactory::setParents() { - for (std::vector::iterator iter = m_Categories.begin(); - iter != m_Categories.end(); ++iter) { - iter->m_HasChildren = false; + for (auto& category : m_Categories) { + category.setHasChildren(false); } - for (std::vector::const_iterator categoryIter = m_Categories.begin(); - categoryIter != m_Categories.end(); ++categoryIter) { - if (categoryIter->m_ParentID != 0) { + for (const auto& category : m_Categories) { + if (category.parentID() != 0) { std::map::const_iterator iter = - m_IDMap.find(categoryIter->m_ParentID); + m_IDMap.find(category.parentID()); if (iter != m_IDMap.end()) { - m_Categories[iter->second].m_HasChildren = true; + m_Categories[iter->second].setHasChildren(true); } } } @@ -139,28 +188,44 @@ 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; } categoryFile.resize(0); - for (std::vector::const_iterator iter = m_Categories.begin(); - iter != m_Categories.end(); ++iter) { - if (iter->m_ID == 0) { + for (const auto& category : m_Categories) { + if (category.ID() == 0) { continue; } QByteArray line; - line.append(QByteArray::number(iter->m_ID)) + line.append(QByteArray::number(category.ID())) .append("|") - .append(iter->m_Name.toUtf8()) + .append(category.name().toUtf8()) .append("|") - .append(VectorJoin(iter->m_NexusIDs, ",").toUtf8()) - .append("|") - .append(QByteArray::number(iter->m_ParentID)) + .append(QByteArray::number(category.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); + for (const auto& nexMap : m_NexusMap) { + QByteArray line; + line.append(QByteArray::number(nexMap.second.categoryID())).append("|"); + line.append(nexMap.second.name().toUtf8()).append("|"); + line.append(QByteArray::number(nexMap.second.ID())).append("\n"); + nexusMapFile.write(line); + } + nexusMapFile.close(); + + emit categoriesSaved(); } unsigned int @@ -175,100 +240,126 @@ CategoryFactory::countCategories(std::function f return result; } -int CategoryFactory::addCategory(const QString& name, const std::vector& nexusIDs, +int CategoryFactory::addCategory(const QString& name, + const std::vector& 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& nexusIDs, int parentID) +void CategoryFactory::addCategory(int id, const QString& name, int parentID) { int index = static_cast(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, std::vector())); + m_IDMap[id] = index; +} + +void CategoryFactory::addCategory(int id, const QString& name, + const std::vector& nexusCats, + int parentID) +{ + for (const auto& nexusCat : nexusCats) { + m_NexusMap.insert_or_assign(nexusCat.ID(), nexusCat); + m_NexusMap.at(nexusCat.ID()).setCategoryID(id); } + int index = static_cast(m_Categories.size()); + m_Categories.push_back(Category(index, id, name, parentID, nexusCats)); m_IDMap[id] = index; } +void CategoryFactory::setNexusCategories( + const std::vector& nexusCats) +{ + for (const auto& nexusCat : nexusCats) { + m_NexusMap.emplace(nexusCat.ID(), nexusCat); + } + + saveCategories(); +} + +void CategoryFactory::refreshNexusCategories(CategoriesDialog* dialog) +{ + emit nexusCategoryRefresh(dialog); +} + 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); + addCategory(1, "Animations", 0); + addCategory(52, "Poses", 1); + addCategory(2, "Armour", 0); + addCategory(53, "Power Armor", 2); + addCategory(3, "Audio", 0); + addCategory(38, "Music", 0); + addCategory(39, "Voice", 0); + addCategory(5, "Clothing", 0); + addCategory(41, "Jewelry", 5); + addCategory(42, "Backpacks", 5); + addCategory(6, "Collectables", 0); + addCategory(28, "Companions", 0); + addCategory(7, "Creatures, Mounts, & Vehicles", 0); + addCategory(8, "Factions", 0); + addCategory(9, "Gameplay", 0); + addCategory(27, "Combat", 9); + addCategory(43, "Crafting", 9); + addCategory(48, "Overhauls", 9); + addCategory(49, "Perks", 9); + addCategory(54, "Radio", 9); + addCategory(55, "Shouts", 9); + addCategory(22, "Skills & Levelling", 9); + addCategory(58, "Weather & Lighting", 9); + addCategory(44, "Equipment", 43); + addCategory(45, "Home/Settlement", 43); + addCategory(10, "Body, Face, & Hair", 0); + addCategory(39, "Tattoos", 10); + addCategory(40, "Character Presets", 0); + addCategory(11, "Items", 0); + addCategory(32, "Mercantile", 0); + addCategory(37, "Ammo", 11); + addCategory(19, "Weapons", 11); + addCategory(36, "Weapon & Armour Sets", 11); + addCategory(23, "Player Homes", 0); + addCategory(25, "Castles & Mansions", 23); + addCategory(51, "Settlements", 23); + addCategory(12, "Locations", 0); + addCategory(4, "Cities", 12); + addCategory(31, "Landscape Changes", 0); + addCategory(29, "Environment", 0); + addCategory(30, "Immersion", 0); + addCategory(20, "Magic", 0); + addCategory(21, "Models & Textures", 0); + addCategory(33, "Modders resources", 0); + addCategory(13, "NPCs", 0); + addCategory(24, "Bugfixes", 0); + addCategory(14, "Patches", 24); + addCategory(35, "Utilities", 0); + addCategory(26, "Cheats", 0); + addCategory(15, "Quests", 0); + addCategory(16, "Races & Classes", 0); + addCategory(34, "Stealth", 0); + addCategory(17, "UI", 0); + addCategory(18, "Visuals", 0); + addCategory(50, "Pip-Boy", 18); + addCategory(46, "Shader Presets", 0); + addCategory(47, "Miscellaneous", 0); } 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; + return m_Categories[index].parentID(); } bool CategoryFactory::categoryExists(int id) const @@ -295,15 +386,15 @@ bool CategoryFactory::isDescendantOfImpl(int id, int parentID, if (iter != m_IDMap.end()) { unsigned int index = iter->second; - if (m_Categories[index].m_ParentID == 0) { + if (m_Categories[index].parentID() == 0) { return false; - } else if (m_Categories[index].m_ParentID == parentID) { + } else if (m_Categories[index].parentID() == parentID) { return true; } else { - return isDescendantOfImpl(m_Categories[index].m_ParentID, parentID, seen); + return isDescendantOfImpl(m_Categories[index].parentID(), parentID, seen); } } else { - log::warn("{} is no valid category id", id); + log::warn(tr("{} is no valid category id").toStdString(), id); return false; } } @@ -311,19 +402,19 @@ 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; + return m_Categories[index].hasChildren(); } 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; + return m_Categories[index].name(); } QString CategoryFactory::getSpecialCategoryName(SpecialCategories type) const @@ -381,24 +472,24 @@ QString CategoryFactory::getCategoryNameByID(int id) const return {}; } - return m_Categories[index].m_Name; + return m_Categories[index].name(); } } 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; + return m_Categories[index].ID(); } int CategoryFactory::getCategoryIndex(int ID) const { std::map::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; } @@ -407,11 +498,11 @@ int CategoryFactory::getCategoryID(const QString& name) const { auto iter = std::find_if(m_Categories.begin(), m_Categories.end(), [name](const Category& cat) -> bool { - return cat.m_Name == name; + return cat.name() == name; }); if (iter != m_Categories.end()) { - return iter->m_ID; + return iter->ID(); } else { return -1; } @@ -419,12 +510,14 @@ int CategoryFactory::getCategoryID(const QString& name) const unsigned int CategoryFactory::resolveNexusID(int nexusID) const { - std::map::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; - } else { - log::debug("nexus category id {} not mapped", nexusID); - return 0U; + auto result = m_NexusMap.find(nexusID); + if (result != m_NexusMap.end()) { + if (m_IDMap.count(result->second.categoryID())) { + log::debug(tr("nexus category id {} maps to internal {}").toStdString(), nexusID, + m_IDMap.at(result->second.categoryID())); + return m_IDMap.at(result->second.categoryID()); + } } + log::debug(tr("nexus category id {} not mapped").toStdString(), nexusID); + return 0U; } diff --git a/src/categories.h b/src/categories.h index b938bd195..15b9bbc17 100644 --- a/src/categories.h +++ b/src/categories.h @@ -25,14 +25,17 @@ along with Mod Organizer. If not, see . #include #include +class CategoriesDialog; + /** * @brief Manage the available mod categories * @warning member functions of this class currently use a wild mix of ids and indexes *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 : public QObject { + Q_OBJECT; friend class CategoriesDialog; @@ -53,24 +56,64 @@ class CategoryFactory }; public: + struct NexusCategory + { + NexusCategory(const QString name, const int nexusID) : m_Name(name), m_ID(nexusID) + {} + + friend bool operator==(const NexusCategory& LHS, const NexusCategory& RHS) + { + return LHS.ID() == RHS.ID(); + } + + friend bool operator==(const NexusCategory& LHS, const int RHS) + { + return LHS.ID() == RHS; + } + + friend bool operator<(const NexusCategory& LHS, const NexusCategory& RHS) + { + return LHS.ID() < RHS.ID(); + } + + QString name() const { return m_Name; } + int ID() const { return m_ID; } + int categoryID() const { return m_CategoryID; } + void setCategoryID(int categoryID) { m_CategoryID = categoryID; } + + private: + QString m_Name; + int m_ID; + int m_CategoryID = -1; + }; + struct Category { - Category(int sortValue, int id, const QString& name, - const std::vector& nexusIDs, int parentID) + Category(int sortValue, int id, const QString name, int parentID, + std::vector nexusCats) : m_SortValue(sortValue), m_ID(id), m_Name(name), m_HasChildren(false), - m_NexusIDs(nexusIDs), m_ParentID(parentID) + m_ParentID(parentID), m_NexusCats(std::move(nexusCats)) {} - int m_SortValue; - int m_ID; - int m_ParentID; - bool m_HasChildren; - QString m_Name; - std::vector m_NexusIDs; friend bool operator<(const Category& LHS, const Category& RHS) { - return LHS.m_SortValue < RHS.m_SortValue; + return LHS.sortValue() < RHS.sortValue(); } + + int sortValue() const { return m_SortValue; } + int ID() const { return m_ID; } + int parentID() const { return m_ParentID; } + QString name() const { return m_Name; } + bool hasChildren() const { return m_HasChildren; } + void setHasChildren(bool b) { m_HasChildren = b; } + + private: + int m_SortValue; + int m_ID; + int m_ParentID; + QString m_Name; + std::vector m_NexusCats; + bool m_HasChildren; }; public: @@ -89,7 +132,12 @@ class CategoryFactory **/ void saveCategories(); - int addCategory(const QString& name, const std::vector& nexusIDs, int parentID); + void setNexusCategories(const std::vector& nexusCats); + + void refreshNexusCategories(CategoriesDialog* dialog); + + int addCategory(const QString& name, const std::vector& nexusCats, + int parentID); /** * @brief retrieve the number of available categories @@ -190,13 +238,23 @@ class CategoryFactory */ static QString categoriesFilePath(); + /** + * @return path to the file that contains the nexus category mappings + */ + static QString nexusMappingFilePath(); + +signals: + void nexusCategoryRefresh(CategoriesDialog*); + void categoriesSaved(); + private: - CategoryFactory(); + explicit CategoryFactory(); void loadDefaultCategories(); - void addCategory(int id, const QString& name, const std::vector& nexusID, - int parentID); + void addCategory(int id, const QString& name, + const std::vector& nexusCats, int parentID); + void addCategory(int id, const QString& name, int parentID); void setParents(); @@ -207,7 +265,7 @@ class CategoryFactory std::vector m_Categories; std::map m_IDMap; - std::map m_NexusMap; + std::map m_NexusMap; private: // called by isDescendantOf() diff --git a/src/categoriesdialog.cpp b/src/categoriesdialog.cpp index 99a931efa..ff2262861 100644 --- a/src/categoriesdialog.cpp +++ b/src/categoriesdialog.cpp @@ -19,6 +19,9 @@ along with Mod Organizer. If not, see . #include "categoriesdialog.h" #include "categories.h" +#include "categoryimportdialog.h" +#include "messagedialog.h" +#include "nexusinterface.h" #include "settings.h" #include "ui_categoriesdialog.h" #include "utility.h" @@ -109,6 +112,14 @@ CategoriesDialog::CategoriesDialog(QWidget* parent) fillTable(); connect(ui->categoriesTable, SIGNAL(cellChanged(int, int)), this, SLOT(cellChanged(int, int))); + if (Settings::instance().nexus().categoryMappings()) { + connect(ui->nexusRefresh, SIGNAL(clicked()), this, SLOT(nexusRefresh_clicked())); + connect(ui->nexusImportButton, SIGNAL(clicked()), this, + SLOT(nexusImport_clicked())); + ui->nexusCategoryList->setDisabled(false); + } else { + ui->nexusCategoryList->setDisabled(true); + } } CategoriesDialog::~CategoriesDialog() @@ -136,21 +147,31 @@ void CategoriesDialog::commitChanges() categories.reset(); for (int i = 0; i < ui->categoriesTable->rowCount(); ++i) { - int index = ui->categoriesTable->verticalHeader()->logicalIndex(i); - QString nexusIDString = ui->categoriesTable->item(index, 2)->text(); - QStringList nexusIDStringList = nexusIDString.split(',', Qt::SkipEmptyParts); - std::vector nexusIDs; - for (QStringList::iterator iter = nexusIDStringList.begin(); - iter != nexusIDStringList.end(); ++iter) { - nexusIDs.push_back(iter->toInt()); + int index = ui->categoriesTable->verticalHeader()->logicalIndex(i); + QVariantList nexusData = + ui->categoriesTable->item(index, 3)->data(Qt::UserRole).toList(); + std::vector nexusCats; + for (auto nexusCat : nexusData) { + nexusCats.push_back(CategoryFactory::NexusCategory( + nexusCat.toList()[0].toString(), nexusCat.toList()[1].toInt())); } categories.addCategory(ui->categoriesTable->item(index, 0)->text().toInt(), - ui->categoriesTable->item(index, 1)->text(), nexusIDs, - ui->categoriesTable->item(index, 3)->text().toInt()); + ui->categoriesTable->item(index, 1)->text(), nexusCats, + ui->categoriesTable->item(index, 2)->text().toInt()); } + categories.setParents(); + std::vector nexusCats; + for (int i = 0; i < ui->nexusCategoryList->count(); ++i) { + nexusCats.push_back(CategoryFactory::NexusCategory( + ui->nexusCategoryList->item(i)->data(Qt::DisplayRole).toString(), + ui->nexusCategoryList->item(i)->data(Qt::UserRole).toInt())); + } + + categories.setNexusCategories(nexusCats); + categories.saveCategories(); } @@ -170,57 +191,66 @@ void CategoriesDialog::fillTable() { CategoryFactory& categories = CategoryFactory::instance(); QTableWidget* table = ui->categoriesTable; + QListWidget* list = ui->nexusCategoryList; -#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed); table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Fixed); - table->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Fixed); + table->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); table->verticalHeader()->setSectionsMovable(true); table->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); -#else - table->horizontalHeader()->setResizeMode(0, QHeaderView::Fixed); - table->horizontalHeader()->setResizeMode(1, QHeaderView::Stretch); - table->horizontalHeader()->setResizeMode(2, QHeaderView::Fixed); - table->horizontalHeader()->setResizeMode(3, QHeaderView::Fixed); - table->verticalHeader()->setMovable(true); - table->verticalHeader()->setResizeMode(QHeaderView::Fixed); -#endif - table->setItemDelegateForColumn( 0, new ValidatingDelegate(this, new NewIDValidator(m_IDs))); table->setItemDelegateForColumn( - 2, new ValidatingDelegate(this, + 2, new ValidatingDelegate(this, new ExistingIDValidator(m_IDs))); + table->setItemDelegateForColumn( + 3, new ValidatingDelegate(this, new QRegularExpressionValidator( QRegularExpression("([0-9]+)?(,[0-9]+)*"), this))); - table->setItemDelegateForColumn( - 3, new ValidatingDelegate(this, new ExistingIDValidator(m_IDs))); int row = 0; - for (std::vector::const_iterator iter = - categories.m_Categories.begin(); - iter != categories.m_Categories.end(); ++iter, ++row) { - const CategoryFactory::Category& category = *iter; - if (category.m_ID == 0) { + for (const auto& category : categories.m_Categories) { + if (category.ID() == 0) { --row; continue; } + ++row; table->insertRow(row); // table->setVerticalHeaderItem(row, new QTableWidgetItem(" ")); QScopedPointer idItem(new QTableWidgetItem()); - idItem->setData(Qt::DisplayRole, category.m_ID); + idItem->setData(Qt::DisplayRole, category.ID()); - QScopedPointer nameItem(new QTableWidgetItem(category.m_Name)); - QScopedPointer nexusIDItem( - new QTableWidgetItem(MOBase::VectorJoin(category.m_NexusIDs, ","))); + QScopedPointer nameItem(new QTableWidgetItem(category.name())); QScopedPointer parentIDItem(new QTableWidgetItem()); - parentIDItem->setData(Qt::DisplayRole, category.m_ParentID); + parentIDItem->setData(Qt::DisplayRole, category.parentID()); + QScopedPointer nexusCatItem(new QTableWidgetItem()); table->setItem(row, 0, idItem.take()); table->setItem(row, 1, nameItem.take()); - table->setItem(row, 2, nexusIDItem.take()); - table->setItem(row, 3, parentIDItem.take()); + table->setItem(row, 2, parentIDItem.take()); + table->setItem(row, 3, nexusCatItem.take()); + } + + for (const auto& nexusCat : categories.m_NexusMap) { + QScopedPointer nexusItem(new QListWidgetItem()); + nexusItem->setData(Qt::DisplayRole, nexusCat.second.name()); + nexusItem->setData(Qt::UserRole, nexusCat.second.ID()); + list->addItem(nexusItem.take()); + auto item = table->item(categories.resolveNexusID(nexusCat.first) - 1, 3); + if (item != nullptr) { + auto itemData = item->data(Qt::UserRole).toList(); + QVariantList newData; + newData.append(nexusCat.second.name()); + newData.append(nexusCat.second.ID()); + itemData.insert(itemData.length(), newData); + QStringList names; + for (auto cat : itemData) { + names.append(cat.toList()[0].toString()); + } + item->setData(Qt::UserRole, itemData); + item->setData(Qt::DisplayRole, names.join(", ")); + } } refreshIDs(); @@ -234,13 +264,112 @@ void CategoriesDialog::addCategory_clicked() ui->categoriesTable->setItem(row, 0, new QTableWidgetItem(QString::number(++m_HighestID))); ui->categoriesTable->setItem(row, 1, new QTableWidgetItem("new")); - ui->categoriesTable->setItem(row, 2, new QTableWidgetItem("")); - ui->categoriesTable->setItem(row, 3, new QTableWidgetItem("0")); + ui->categoriesTable->setItem(row, 2, new QTableWidgetItem("0")); + ui->categoriesTable->setItem(row, 3, new QTableWidgetItem("")); } void CategoriesDialog::removeCategory_clicked() { - ui->categoriesTable->removeRow(m_ContextRow); + if (m_ContextRow >= 0) + ui->categoriesTable->removeRow(m_ContextRow); +} + +void CategoriesDialog::removeNexusMap_clicked() +{ + if (m_ContextRow >= 0) { + ui->categoriesTable->item(m_ContextRow, 3)->setData(Qt::UserRole, QVariantList()); + ui->categoriesTable->item(m_ContextRow, 3)->setData(Qt::DisplayRole, QString()); + } +} + +void CategoriesDialog::nexusRefresh_clicked() +{ + CategoryFactory::instance().refreshNexusCategories(this); +} + +void CategoriesDialog::nexusImport_clicked() +{ + auto importDialog = CategoryImportDialog(this); + if (importDialog.exec() && importDialog.strategy()) { + refreshIDs(); + QTableWidget* table = ui->categoriesTable; + QListWidget* list = ui->nexusCategoryList; + if (importDialog.strategy() == CategoryImportDialog::Overwrite) { + table->setRowCount(0); + m_HighestID = 0; + } + int row = 0; + for (int i = 0; i < list->count(); ++i) { + QString name = list->item(i)->data(Qt::DisplayRole).toString(); + int nexusID = list->item(i)->data(Qt::UserRole).toInt(); + QStringList nexusLabel; + QVariantList nexusData; + nexusLabel.append(name); + QVariantList data; + data.append(QVariant(name)); + data.append(QVariant(nexusID)); + nexusData.insert(nexusData.size(), data); + QScopedPointer nexusCatItem( + new QTableWidgetItem(nexusLabel.join(", "))); + nexusCatItem->setData(Qt::UserRole, nexusData); + if (!table->findItems(name, Qt::MatchExactly).size()) { + row = table->rowCount(); + table->insertRow(table->rowCount()); + // table->setVerticalHeaderItem(row, new QTableWidgetItem(" ")); + + QScopedPointer idItem(new QTableWidgetItem()); + idItem->setData(Qt::DisplayRole, ++m_HighestID); + + QScopedPointer nameItem(new QTableWidgetItem(name)); + QScopedPointer parentIDItem(new QTableWidgetItem()); + parentIDItem->setData(Qt::DisplayRole, 0); // No parent + + table->setItem(row, 0, idItem.take()); + table->setItem(row, 1, nameItem.take()); + table->setItem(row, 2, parentIDItem.take()); + + if (importDialog.assign()) { + table->setItem(row, 3, nexusCatItem.take()); + } + } else { + for (auto item : table->findItems(name, Qt::MatchContains | Qt::MatchWrap)) { + if (item->column() == 1 && item->text() == name && importDialog.remap()) { + table->setItem(item->row(), 3, nexusCatItem.take()); + } else if (importDialog.remap()) { + QScopedPointer blankItem(new QTableWidgetItem()); + blankItem->setData(Qt::UserRole, QVariantList()); + table->setItem(item->row(), 3, blankItem.get()); + } + } + } + } + refreshIDs(); + } +} + +void CategoriesDialog::nxmGameInfoAvailable(QString gameName, QVariant, + QVariant resultData, int) +{ + QVariantMap result = resultData.toMap(); + QVariantList categories = result["categories"].toList(); + CategoryFactory& catFactory = CategoryFactory::instance(); + QListWidget* list = ui->nexusCategoryList; + list->clear(); + for (const auto& category : categories) { + auto catMap = category.toMap(); + QScopedPointer nexusItem(new QListWidgetItem()); + nexusItem->setData(Qt::DisplayRole, catMap["name"].toString()); + nexusItem->setData(Qt::UserRole, catMap["category_id"].toInt()); + list->addItem(nexusItem.take()); + } +} + +void CategoriesDialog::nxmRequestFailed(QString, int, int, QVariant, int, int errorCode, + const QString& errorMessage) +{ + MessageDialog::showMessage( + tr("Error %1: Request to Nexus failed: %2").arg(errorCode).arg(errorMessage), + this); } void CategoriesDialog::on_categoriesTable_customContextMenuRequested(const QPoint& pos) @@ -249,6 +378,9 @@ void CategoriesDialog::on_categoriesTable_customContextMenuRequested(const QPoin QMenu menu; menu.addAction(tr("Add"), this, SLOT(addCategory_clicked())); menu.addAction(tr("Remove"), this, SLOT(removeCategory_clicked())); + if (Settings::instance().nexus().categoryMappings()) { + menu.addAction(tr("Remove Nexus Mapping(s)"), this, SLOT(removeNexusMap_clicked())); + } menu.exec(ui->categoriesTable->mapToGlobal(pos)); } diff --git a/src/categoriesdialog.h b/src/categoriesdialog.h index 2ac84124e..94f390b07 100644 --- a/src/categoriesdialog.h +++ b/src/categoriesdialog.h @@ -20,6 +20,8 @@ along with Mod Organizer. If not, see . #ifndef CATEGORIESDIALOG_H #define CATEGORIESDIALOG_H +#include "categories.h" +#include "plugincontainer.h" #include "tutorabledialog.h" #include @@ -49,11 +51,18 @@ class CategoriesDialog : public MOBase::TutorableDialog **/ void commitChanges(); -private slots: +public slots: + void nxmGameInfoAvailable(QString gameName, QVariant, QVariant resultData, int); + void nxmRequestFailed(QString, int, int, QVariant, int, int errorCode, + const QString& errorMessage); +private slots: void on_categoriesTable_customContextMenuRequested(const QPoint& pos); void addCategory_clicked(); void removeCategory_clicked(); + void removeNexusMap_clicked(); + void nexusRefresh_clicked(); + void nexusImport_clicked(); void cellChanged(int row, int column); private: @@ -62,10 +71,12 @@ private slots: private: Ui::CategoriesDialog* ui; + PluginContainer* m_PluginContainer; int m_ContextRow; int m_HighestID; std::set m_IDs; + std::vector m_NexusCategories; }; #endif // CATEGORIESDIALOG_H diff --git a/src/categoriesdialog.ui b/src/categoriesdialog.ui index 582a76082..4f96d8a6b 100644 --- a/src/categoriesdialog.ui +++ b/src/categoriesdialog.ui @@ -6,19 +6,58 @@ 0 0 - 553 - 444 + 711 + 434 Categories - - - + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Refresh from Nexus + + + + + + + + 0 + 0 + + + + <-- Import Nexus Cats + + + + + + + + 0 + 0 + + Qt::CustomContextMenu + + true + false @@ -26,7 +65,7 @@ false - QAbstractItemView::NoDragDrop + QAbstractItemView::DropOnly Qt::IgnoreAction @@ -40,21 +79,24 @@ Qt::DashLine + + true + false true + + 26 + 100 true - - 26 - false @@ -77,51 +119,93 @@ Name - Name of the Categorie used for display. + The display name of the category. - Name of the Categorie used for display. + The display name of the category. - Nexus IDs + Parent ID - Comma-Separated list of Nexus IDs to be matched to the internal ID. - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">You can match one or multiple nexus categories to a internal ID. Whenever you download a mod from a Nexus Page, Mod Organizer will try to resolve the category defined on the Nexus to one available in MO.</span></p> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:8pt;"></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">To find out a category id used by the nexus, visit the categories list of the nexus page and hover over the links there.</span></p></body></html> + If set, the category is defined as a sub-category of another one. Parent ID needs to be a valid category ID. - Parent ID + Nexus Categories - If set, the category is defined as a sub-category of another one. Parent ID needs to be a valid category ID. + Comma-Separated list of Nexus IDs to be matched to the internal ID. + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> + <html><head><meta name="qrichtext" content="1" /><style type="text/css"> + p, li { white-space: pre-wrap; } + </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> + <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">You can match one or multiple nexus categories to a internal ID. Whenever you download a mod from a Nexus Page, Mod Organizer will try to resolve the category defined on the Nexus to one available in MO.</span></p> + <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:8pt;"></p> + <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">To find out a category id used by the nexus, visit the categories list of the nexus page and hover over the links there.</span></p></body></html> + - - - - Qt::Horizontal + + + + + 0 + 0 + - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + 16777215 + 16777215 + + + + Drag & drop nexus categories from this pane onto the target category on the left. + + + Qt::LeftToRight + + + Nexus Categories (Drag && Drop to Assign) + + + + + + 0 + 0 + + + + true + + + QAbstractItemView::DragOnly + + + + + + + CategoriesTable + QTableWidget +
categoriestable.h
+
+
diff --git a/src/categoriestable.cpp b/src/categoriestable.cpp new file mode 100644 index 000000000..fc53fb585 --- /dev/null +++ b/src/categoriestable.cpp @@ -0,0 +1,77 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "categoriestable.h" + +CategoriesTable::CategoriesTable(QWidget* parent) : QTableWidget(parent) {} + +bool CategoriesTable::dropMimeData(int row, int column, const QMimeData* data, + Qt::DropAction action) +{ + if (row == -1) + return false; + + if (action == Qt::IgnoreAction) + return true; + + if (!data->hasFormat("application/x-qabstractitemmodeldatalist")) + return false; + + QByteArray encoded = data->data("application/x-qabstractitemmodeldatalist"); + QDataStream stream(&encoded, QIODevice::ReadOnly); + + while (!stream.atEnd()) { + int curRow, curCol; + QMap roleDataMap; + stream >> curRow >> curCol >> roleDataMap; + + for (auto item : findItems(roleDataMap.value(Qt::DisplayRole).toString(), + Qt::MatchContains | Qt::MatchWrap)) { + if (item->column() != 3) + continue; + QVariantList newData; + for (auto nexData : item->data(Qt::UserRole).toList()) { + if (nexData.toList()[1].toInt() != roleDataMap.value(Qt::UserRole)) { + newData.insert(newData.length(), nexData); + } + } + QStringList names; + for (auto nexData : newData) { + names.append(nexData.toList()[0].toString()); + } + item->setData(Qt::DisplayRole, names.join(", ")); + item->setData(Qt::UserRole, newData); + } + + auto nexusItem = item(row, 3); + auto itemData = nexusItem->data(Qt::UserRole).toList(); + QVariantList newData; + newData.append(roleDataMap.value(Qt::DisplayRole).toString()); + newData.append(roleDataMap.value(Qt::UserRole).toInt()); + itemData.insert(itemData.length(), newData); + QStringList names; + for (auto cat : itemData) { + names.append(cat.toList()[0].toString()); + } + nexusItem->setData(Qt::UserRole, itemData); + nexusItem->setData(Qt::DisplayRole, names.join(", ")); + } + + return true; +} diff --git a/src/categoriestable.h b/src/categoriestable.h new file mode 100644 index 000000000..8ec797dee --- /dev/null +++ b/src/categoriestable.h @@ -0,0 +1,37 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef CATEGORIESTABLE_H +#define CATEGORIESTABLE_H + +#include +#include + +class CategoriesTable : public QTableWidget +{ + Q_OBJECT +public: + CategoriesTable(QWidget* parent = 0); + +protected: + virtual bool dropMimeData(int row, int column, const QMimeData* data, + Qt::DropAction action); +}; + +#endif // CATEGORIESTABLE_H diff --git a/src/categoryimportdialog.cpp b/src/categoryimportdialog.cpp new file mode 100644 index 000000000..4b09c391d --- /dev/null +++ b/src/categoryimportdialog.cpp @@ -0,0 +1,75 @@ +#include "categoryimportdialog.h" +#include "ui_categoryimportdialog.h" + +#include "organizercore.h" + +using namespace MOBase; + +CategoryImportDialog::CategoryImportDialog(QWidget* parent) + : QDialog(parent), ui(new Ui::CategoryImportDialog) +{ + ui->setupUi(this); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, + &CategoryImportDialog::accepted); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, + &CategoryImportDialog::rejected); + connect(ui->strategyGroup, &QButtonGroup::buttonClicked, this, + &CategoryImportDialog::on_strategyClicked); + connect(ui->assignOption, &QCheckBox::clicked, this, + &CategoryImportDialog::on_assignOptionClicked); +} + +void CategoryImportDialog::accepted() +{ + accept(); +} + +void CategoryImportDialog::rejected() +{ + reject(); +} + +CategoryImportDialog::~CategoryImportDialog() +{ + delete ui; +} + +CategoryImportDialog::ImportStrategy CategoryImportDialog::strategy() +{ + if (ui->mergeOption->isChecked()) { + return ImportStrategy::Merge; + } else if (ui->replaceOption->isChecked()) { + return ImportStrategy::Overwrite; + } + return ImportStrategy::None; +} + +bool CategoryImportDialog::assign() +{ + return ui->assignOption->isChecked(); +} + +bool CategoryImportDialog::remap() +{ + return ui->remapOption->isChecked(); +} + +void CategoryImportDialog::on_strategyClicked(QAbstractButton* button) +{ + if (button == ui->replaceOption) { + ui->remapOption->setChecked(false); + ui->remapOption->setDisabled(true); + } else { + ui->remapOption->setEnabled(true); + } +} + +void CategoryImportDialog::on_assignOptionClicked(bool checked) +{ + if (checked && strategy() == ImportStrategy::Merge) { + ui->remapOption->setEnabled(true); + } else { + ui->remapOption->setChecked(false); + ui->remapOption->setDisabled(true); + } +} diff --git a/src/categoryimportdialog.h b/src/categoryimportdialog.h new file mode 100644 index 000000000..bfc2eee49 --- /dev/null +++ b/src/categoryimportdialog.h @@ -0,0 +1,44 @@ +#ifndef CATEGORYIMPORTDIALOG_H +#define CATEGORYIMPORTDIALOG_H + +#include + +namespace Ui +{ +class CategoryImportDialog; +} + +/** + * @brief Dialog that allows users to configure mod categories + **/ +class CategoryImportDialog : public QDialog +{ + Q_OBJECT + +public: + enum ImportStrategy + { + None, + Overwrite, + Merge + }; + +public: + explicit CategoryImportDialog(QWidget* parent = 0); + ~CategoryImportDialog(); + + ImportStrategy strategy(); + bool assign(); + bool remap(); + +public slots: + void accepted(); + void rejected(); + void on_strategyClicked(QAbstractButton* button); + void on_assignOptionClicked(bool clicked); + +private: + Ui::CategoryImportDialog* ui; +}; + +#endif // CATEGORYIMPORTDIALOG_H diff --git a/src/categoryimportdialog.ui b/src/categoryimportdialog.ui new file mode 100644 index 000000000..c3a18ab1e --- /dev/null +++ b/src/categoryimportdialog.ui @@ -0,0 +1,144 @@ + + + CategoryImportDialog + + + + 0 + 0 + 400 + 193 + + + + Nexus Category Import + + + + + + + 0 + 0 + + + + <h3>How do you want to import the categories?</h3> + + + + + + + + + + 0 + 0 + + + + Import Strategy + + + + + + Merge + + + strategyGroup + + + + + + + Replace + + + true + + + strategyGroup + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Options + + + + + + Assign nexus mappings + + + true + + + + + + + false + + + Remap existing mappings + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + diff --git a/src/filterlist.cpp b/src/filterlist.cpp index d0c7199ec..3be67def3 100644 --- a/src/filterlist.cpp +++ b/src/filterlist.cpp @@ -2,6 +2,7 @@ #include "categories.h" #include "categoriesdialog.h" #include "organizercore.h" +#include "plugincontainer.h" #include "settings.h" #include "ui_mainwindow.h" #include diff --git a/src/installationmanager.cpp b/src/installationmanager.cpp index 8c2ce78d3..37c86c7e4 100644 --- a/src/installationmanager.cpp +++ b/src/installationmanager.cpp @@ -645,6 +645,7 @@ InstallationResult InstallationManager::install(const QString& fileName, QString gameName = ""; QString version = ""; QString newestVersion = ""; + int category = 0; int categoryID = 0; int fileCategoryID = 1; QString repository = "Nexus"; @@ -661,9 +662,31 @@ InstallationResult InstallationManager::install(const QString& fileName, version = metaFile.value("version", "").toString(); newestVersion = metaFile.value("newestVersion", "").toString(); - unsigned int categoryIndex = CategoryFactory::instance().resolveNexusID( - metaFile.value("category", 0).toInt()); - categoryID = CategoryFactory::instance().getCategoryID(categoryIndex); + category = metaFile.value("category", 0).toInt(); + unsigned int categoryIndex = CategoryFactory::instance().resolveNexusID(category); + if (category != 0 && categoryIndex == 0U && + Settings::instance().nexus().categoryMappings()) { + QMessageBox nexusQuery; + nexusQuery.setWindowTitle(tr("No category found")); + nexusQuery.setText(tr( + "This Nexus category has not yet been mapped. Do you wish to proceed without " + "setting a category, proceed and disable automatic Nexus mappings, or stop " + "and configure your category mappings?")); + QPushButton* proceedButton = + nexusQuery.addButton(tr("&Proceed"), QMessageBox::YesRole); + QPushButton* disableButton = + nexusQuery.addButton(tr("&Disable"), QMessageBox::AcceptRole); + QPushButton* stopButton = + nexusQuery.addButton(tr("&Stop && Configure"), QMessageBox::DestructiveRole); + nexusQuery.exec(); + if (nexusQuery.clickedButton() == disableButton) { + Settings::instance().nexus().setCategoryMappings(false); + } else if (nexusQuery.clickedButton() == stopButton) { + return MOBase::IPluginInstaller::RESULT_CATEGORYREQUESTED; + } + } else { + categoryID = CategoryFactory::instance().getCategoryID(categoryIndex); + } repository = metaFile.value("repository", "").toString(); fileCategoryID = metaFile.value("fileCategory", 1).toInt(); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ebe7da98d..ce2c4dd04 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -290,6 +290,8 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, ui->statusBar->setAPI(ni.getAPIStats(), ni.getAPIUserAccount()); } + m_CategoryFactory.loadCategories(); + ui->logList->setCore(m_OrganizerCore); setupToolbar(); @@ -453,6 +455,10 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, connect(&m_OrganizerCore, &OrganizerCore::modInstalled, this, &MainWindow::modInstalled); + connect(&m_CategoryFactory, SIGNAL(nexusCategoryRefresh(CategoriesDialog*)), this, + SLOT(refreshNexusCategories(CategoriesDialog*))); + connect(&m_CategoryFactory, SIGNAL(categoriesSaved()), this, SLOT(categoriesSaved())); + m_CheckBSATimer.setSingleShot(true); connect(&m_CheckBSATimer, SIGNAL(timeout()), this, SLOT(checkBSAList())); @@ -1266,7 +1272,78 @@ void MainWindow::showEvent(QShowEvent* event) gameSupportTriggered(); } + QMessageBox newCatDialog; + newCatDialog.setWindowTitle(tr("Category Setup")); + newCatDialog.setText( + tr("Please choose how to handle the default category setup.\n\n" + "If you've already connected to Nexus, you can automatically import Nexus " + "categories for this game (if applicable). Otherwise, use the old Mod " + "Organizer default category structure, or leave the categories blank (for " + "manual setup).")); + QPushButton importBtn(tr("&Import Nexus Categories")); + QPushButton defaultBtn(tr("Use &Old Category Defaults")); + QPushButton cancelBtn(tr("Do &Nothing")); + if (NexusInterface::instance().getAccessManager()->validated()) { + newCatDialog.addButton(&importBtn, QMessageBox::ButtonRole::AcceptRole); + } + newCatDialog.addButton(&defaultBtn, QMessageBox::ButtonRole::AcceptRole); + newCatDialog.addButton(&cancelBtn, QMessageBox::ButtonRole::RejectRole); + newCatDialog.exec(); + if (newCatDialog.clickedButton() == &importBtn) { + importCategories(false); + } else if (newCatDialog.clickedButton() == &cancelBtn) { + m_CategoryFactory.reset(); + } else if (newCatDialog.clickedButton() == &defaultBtn) { + m_CategoryFactory.loadCategories(); + } + m_CategoryFactory.saveCategories(); + m_OrganizerCore.settings().setFirstStart(false); + } else { + auto& settings = m_OrganizerCore.settings(); + if (m_LastVersion < QVersionNumber(2, 5) && + !GlobalSettings::hideCategoryReminder()) { + QMessageBox migrateCatDialog; + migrateCatDialog.setWindowTitle("Category Migration"); + migrateCatDialog.setText( + tr("This is your first time running version 2.5 or higher with an old MO2 " + "instance. The category system now relies on an updated system to map " + "Nexus categories.\n\n" + "In order to assign Nexus categories automatically, you will need to " + "import the Nexus categories for the currently managed game and map " + "them to your preferred category structure.\n\n" + "You can either manually open the category editor, via the Settings " + "dialog or the category filter sidebar, and set up the mappings as you " + "see fit, or you can automatically import and map the categories as " + "defined on Nexus.\n\n" + "As a final option, you can disable Nexus category mapping altogether, " + "which can be changed at any time in the Settings dialog.")); + QPushButton importBtn(tr("&Import Nexus Categories")); + QPushButton openSettingsBtn(tr("&Open Categories Dialog")); + QPushButton disableBtn(tr("&Disable Nexus Mappings")); + QPushButton closeBtn(tr("&Close")); + QCheckBox dontShow(tr("&Don't show this again")); + if (NexusInterface::instance().getAccessManager()->validated()) { + migrateCatDialog.addButton(&importBtn, QMessageBox::ButtonRole::AcceptRole); + } + migrateCatDialog.addButton(&openSettingsBtn, + QMessageBox::ButtonRole::ActionRole); + migrateCatDialog.addButton(&disableBtn, + QMessageBox::ButtonRole::DestructiveRole); + migrateCatDialog.addButton(&closeBtn, QMessageBox::ButtonRole::RejectRole); + migrateCatDialog.setCheckBox(&dontShow); + migrateCatDialog.exec(); + if (migrateCatDialog.clickedButton() == &importBtn) { + importCategories(dontShow.isChecked()); + } else if (migrateCatDialog.clickedButton() == &openSettingsBtn) { + this->ui->filtersEdit->click(); + } else if (migrateCatDialog.clickedButton() == &disableBtn) { + Settings::instance().nexus().setCategoryMappings(false); + } + if (dontShow.isChecked()) { + GlobalSettings::setHideCategoryReminder(true); + } + } } m_OrganizerCore.settings().widgets().restoreIndex(ui->groupCombo); @@ -2097,6 +2174,8 @@ void MainWindow::processUpdates() const auto lastVersion = settings.version().value_or(earliest); const auto currentVersion = m_OrganizerCore.getVersion().asQVersionNumber(); + m_LastVersion = lastVersion; + settings.processUpdates(currentVersion, lastVersion); if (!settings.firstStart()) { @@ -2371,6 +2450,14 @@ void MainWindow::modInstalled(const QString& modName) {m_OrganizerCore.modList()->index(index, 0)}); } +void MainWindow::importCategories(bool) +{ + NexusInterface& nexus = NexusInterface::instance(); + nexus.setPluginContainer(&m_OrganizerCore.pluginContainer()); + nexus.requestGameInfo(Settings::instance().game().plugin()->gameShortName(), this, + QVariant(), QString()); +} + void MainWindow::showMessage(const QString& message) { MessageDialog::showMessage(message, this); @@ -2745,6 +2832,25 @@ void MainWindow::onPluginRegistrationChanged() m_DownloadsTab->update(); } +void MainWindow::refreshNexusCategories(CategoriesDialog* dialog) +{ + NexusInterface& nexus = NexusInterface::instance(); + nexus.setPluginContainer(&m_PluginContainer); + nexus.requestGameInfo(Settings::instance().game().plugin()->gameShortName(), dialog, + QVariant(), QString()); +} + +void MainWindow::categoriesSaved() +{ + for (auto modName : m_OrganizerCore.modList()->allMods()) { + auto mod = ModInfo::getByName(modName); + for (auto category : mod->getCategories()) { + if (!m_CategoryFactory.categoryExists(category)) + mod->setCategory(category, false); + } + } +} + void MainWindow::on_actionNexus_triggered() { const IPluginGame* game = m_OrganizerCore.managedGame(); @@ -3220,6 +3326,8 @@ void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userD mod->setNexusDescription(result["description"].toString()); + mod->setNexusCategory(result["category_id"].toInt()); + if ((mod->endorsedState() != EndorsedState::ENDORSED_NEVER) && (result.contains("endorsement"))) { QVariantMap endorsement = result["endorsement"].toMap(); @@ -3353,6 +3461,22 @@ void MainWindow::nxmDownloadURLs(QString, int, int, QVariant, QVariant resultDat m_OrganizerCore.settings().network().updateServers(servers); } +void MainWindow::nxmGameInfoAvailable(QString gameName, QVariant, QVariant resultData, + int) +{ + QVariantMap result = resultData.toMap(); + QVariantList categories = result["categories"].toList(); + CategoryFactory& catFactory = CategoryFactory::instance(); + catFactory.reset(); + for (auto category : categories) { + auto catMap = category.toMap(); + std::vector nexusCat; + nexusCat.push_back(CategoryFactory::NexusCategory(catMap["name"].toString(), + catMap["category_id"].toInt())); + catFactory.addCategory(catMap["name"].toString(), nexusCat, 0); + } +} + void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, int, int errorCode, const QString& errorString) { diff --git a/src/mainwindow.h b/src/mainwindow.h index 8cd55c901..99feca83b 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -317,6 +317,8 @@ private slots: std::atomic m_ProblemsCheckRequired; std::mutex m_CheckForProblemsMutex; + QVersionNumber m_LastVersion; + Executable* getSelectedExecutable(); private slots: @@ -359,6 +361,11 @@ private slots: void modInstalled(const QString& modName); + void importCategories(bool); + + void refreshNexusCategories(CategoriesDialog* dialog); + void categoriesSaved(); + // update info void nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int requestID); @@ -372,6 +379,7 @@ private slots: void nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int); void nxmDownloadURLs(QString, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmGameInfoAvailable(QString gameName, QVariant, QVariant resultData, int); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, int errorCode, const QString& errorString); diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 1d30ac952..1028c66f1 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -495,7 +495,8 @@ void ModInfo::addCategory(const QString& categoryName) { int id = CategoryFactory::instance().getCategoryID(categoryName); if (id == -1) { - id = CategoryFactory::instance().addCategory(categoryName, std::vector(), 0); + id = CategoryFactory::instance().addCategory( + categoryName, std::vector(), 0); } setCategory(id, true); } diff --git a/src/modinfo.h b/src/modinfo.h index 9895d44c6..d9e87ae05 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -888,6 +888,16 @@ class ModInfo : public QObject, public MOBase::IModInterface */ virtual void setNexusLastModified(QDateTime time) = 0; + /** + * @return the assigned nexus category ID + */ + virtual int getNexusCategory() const = 0; + + /** + * @brief Assigns the given Nexus category ID + */ + virtual void setNexusCategory(int category) = 0; + public: // Conflicts // retrieve the list of mods (as mod index) that are overwritten by this one. // Updates may be delayed. diff --git a/src/modinfobackup.h b/src/modinfobackup.h index 01abf1845..0c3c29d77 100644 --- a/src/modinfobackup.h +++ b/src/modinfobackup.h @@ -38,6 +38,8 @@ class ModInfoBackup : public ModInfoRegular virtual QDateTime getNexusLastModified() const override { return QDateTime(); } virtual void setNexusLastModified(QDateTime) override {} virtual QString getNexusDescription() const override { return QString(); } + virtual void setNexusCategory(int) override {} + virtual int getNexusCategory() const override { return 0; } virtual bool isBackup() const override { return true; } virtual void addInstalledFile(int, int) override {} diff --git a/src/modinfodialog.ui b/src/modinfodialog.ui index 269aebe3f..53655aa53 100644 --- a/src/modinfodialog.ui +++ b/src/modinfodialog.ui @@ -966,7 +966,7 @@ text-align: left; 0 - + @@ -1026,6 +1026,29 @@ p, li { white-space: pre-wrap; } + + + + Category + + + + + + + + 1 + 0 + + + + + 0 + 0 + + + + diff --git a/src/modinfodialognexus.cpp b/src/modinfodialognexus.cpp index 566361458..485f138a9 100644 --- a/src/modinfodialognexus.cpp +++ b/src/modinfodialognexus.cpp @@ -32,6 +32,9 @@ NexusTab::NexusTab(ModInfoDialogTabContext cx) connect(ui->version, &QLineEdit::editingFinished, [&] { onVersionChanged(); }); + connect(ui->category, &QLineEdit::editingFinished, [&] { + onCategoryChanged(); + }); connect(ui->refresh, &QPushButton::clicked, [&] { onRefreshBrowser(); @@ -75,6 +78,7 @@ void NexusTab::clear() ui->modID->clear(); ui->sourceGame->clear(); ui->version->clear(); + ui->category->clear(); ui->browser->setPage(new NexusTabWebpage(ui->browser)); ui->hasCustomURL->setChecked(false); ui->customURL->clear(); @@ -108,6 +112,8 @@ void NexusTab::update() ui->sourceGame->setCurrentIndex(ui->sourceGame->findData(gameName)); + ui->category->setText(QString("%1").arg(mod().getNexusCategory())); + auto* page = new NexusTabWebpage(ui->browser); ui->browser->setPage(page); @@ -360,6 +366,16 @@ void NexusTab::onVersionChanged() updateVersionColor(); } +void NexusTab::onCategoryChanged() +{ + if (m_loading) { + return; + } + + int category = ui->category->text().toInt(); + mod().setNexusCategory(category); +} + void NexusTab::onRefreshBrowser() { const auto modID = mod().nexusId(); diff --git a/src/modinfodialognexus.h b/src/modinfodialognexus.h index e9a8720f3..e752018a4 100644 --- a/src/modinfodialognexus.h +++ b/src/modinfodialognexus.h @@ -57,6 +57,7 @@ class NexusTab : public ModInfoDialogTab void onModIDChanged(); void onSourceGameChanged(); void onVersionChanged(); + void onCategoryChanged(); void onRefreshBrowser(); void onVisitNexus(); diff --git a/src/modinfoforeign.h b/src/modinfoforeign.h index 2c956fb7f..dc66e1b04 100644 --- a/src/modinfoforeign.h +++ b/src/modinfoforeign.h @@ -60,6 +60,8 @@ class ModInfoForeign : public ModInfoWithConflictInfo virtual void setNexusFileStatus(int) override {} virtual QDateTime getLastNexusUpdate() const override { return QDateTime(); } virtual void setLastNexusUpdate(QDateTime) override {} + virtual int getNexusCategory() const override { return 0; } + virtual void setNexusCategory(int) override {} virtual QDateTime getLastNexusQuery() const override { return QDateTime(); } virtual void setLastNexusQuery(QDateTime) override {} virtual QDateTime getNexusLastModified() const override { return QDateTime(); } diff --git a/src/modinfooverwrite.h b/src/modinfooverwrite.h index 02154902a..45b5bbdd0 100644 --- a/src/modinfooverwrite.h +++ b/src/modinfooverwrite.h @@ -68,6 +68,8 @@ class ModInfoOverwrite : public ModInfoWithConflictInfo virtual QDateTime getNexusLastModified() const override { return QDateTime(); } virtual void setNexusLastModified(QDateTime) override {} virtual QString getNexusDescription() const override { return QString(); } + virtual void setNexusCategory(int) override {} + virtual int getNexusCategory() const override { return 0; } virtual QStringList archives(bool checkOnDisk = false) override; virtual void addInstalledFile(int, int) override {} virtual std::set> installedFiles() const override { return {}; } diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 4c1004e6b..c79fc5740 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -97,6 +97,7 @@ void ModInfoRegular::readMeta() m_InstallationFile = metaFile.value("installationFile", "").toString(); m_NexusDescription = metaFile.value("nexusDescription", "").toString(); m_NexusFileStatus = metaFile.value("nexusFileStatus", "1").toInt(); + m_NexusCategory = metaFile.value("nexusCategory", 0).toInt(); m_Repository = metaFile.value("repository", "Nexus").toString(); m_Converted = metaFile.value("converted", false).toBool(); m_Validated = metaFile.value("validated", false).toBool(); @@ -171,10 +172,11 @@ void ModInfoRegular::readMeta() m_NexusLastModified = QDateTime::fromString( metaFile.value("nexusLastModified", QDateTime::currentDateTimeUtc()).toString(), Qt::ISODate); - m_Color = metaFile.value("color", QColor()).value(); - m_TrackedState = metaFile.value("tracked", false).toBool() - ? TrackedState::TRACKED_TRUE - : TrackedState::TRACKED_FALSE; + m_NexusCategory = metaFile.value("nexusCategory", 0).toInt(); + m_Color = metaFile.value("color", QColor()).value(); + m_TrackedState = metaFile.value("tracked", false).toBool() + ? TrackedState::TRACKED_TRUE + : TrackedState::TRACKED_FALSE; if (metaFile.contains("endorsed")) { if (metaFile.value("endorsed").canConvert()) { using ut = std::underlying_type_t; @@ -267,6 +269,7 @@ void ModInfoRegular::saveMeta() metaFile.setValue("lastNexusQuery", m_LastNexusQuery.toString(Qt::ISODate)); metaFile.setValue("lastNexusUpdate", m_LastNexusUpdate.toString(Qt::ISODate)); metaFile.setValue("nexusLastModified", m_NexusLastModified.toString(Qt::ISODate)); + metaFile.setValue("nexusCategory", m_NexusCategory); metaFile.setValue("converted", m_Converted); metaFile.setValue("validated", m_Validated); metaFile.setValue("color", m_Color); @@ -834,6 +837,18 @@ void ModInfoRegular::setNexusLastModified(QDateTime time) emit modDetailsUpdated(true); } +int ModInfoRegular::getNexusCategory() const +{ + return m_NexusCategory; +} + +void ModInfoRegular::setNexusCategory(int category) +{ + m_NexusCategory = category; + m_MetaInfoChanged = true; + saveMeta(); +} + void ModInfoRegular::setCustomURL(QString const& url) { m_CustomURL = url; diff --git a/src/modinforegular.h b/src/modinforegular.h index 6c85d66c3..a408d966a 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -379,6 +379,16 @@ class ModInfoRegular : public ModInfoWithConflictInfo */ virtual void setNexusLastModified(QDateTime time) override; + /** + * @return the assigned nexus category ID + */ + virtual int getNexusCategory() const override; + + /** + * @brief Assigns the given Nexus category ID + */ + virtual void setNexusCategory(int category) override; + virtual QStringList archives(bool checkOnDisk = false) override; virtual void setColor(QColor color) override; @@ -457,6 +467,7 @@ private slots: QDateTime m_LastNexusQuery; QDateTime m_LastNexusUpdate; QDateTime m_NexusLastModified; + int m_NexusCategory; QColor m_Color; diff --git a/src/modinfoseparator.h b/src/modinfoseparator.h index c34dd92bc..67bc3d561 100644 --- a/src/modinfoseparator.h +++ b/src/modinfoseparator.h @@ -45,6 +45,8 @@ class ModInfoSeparator : public ModInfoRegular virtual void setLastNexusQuery(QDateTime) override {} virtual QDateTime getNexusLastModified() const override { return QDateTime(); } virtual void setNexusLastModified(QDateTime) override {} + virtual int getNexusCategory() const override { return 0; } + virtual void setNexusCategory(int) override {} virtual QDateTime creationTime() const override { return QDateTime(); } virtual QString getNexusDescription() const override { return QString(); } virtual void addInstalledFile(int /*modId*/, int /*fileId*/) override {} diff --git a/src/modlistcontextmenu.cpp b/src/modlistcontextmenu.cpp index e88cadc8a..06cd19d6d 100644 --- a/src/modlistcontextmenu.cpp +++ b/src/modlistcontextmenu.cpp @@ -94,13 +94,16 @@ void ModListGlobalContextMenu::populate(OrganizerCore& core, ModListView* view, addAction(tr("Check for updates"), [=]() { view->actions().checkModsForUpdates(); }); + addAction(tr("Auto assign categories"), [=]() { + view->actions().assignCategories(); + }); addAction(tr("Refresh"), &core, &OrganizerCore::profileRefresh); addAction(tr("Export to csv..."), [=]() { view->actions().exportModListCSV(); }); } -ModListChangeCategoryMenu::ModListChangeCategoryMenu(CategoryFactory& categories, +ModListChangeCategoryMenu::ModListChangeCategoryMenu(CategoryFactory* categories, ModInfo::Ptr mod, QMenu* parent) : QMenu(tr("Change Categories"), parent) { @@ -131,24 +134,24 @@ ModListChangeCategoryMenu::categories(const QMenu* menu) const return cats; } -bool ModListChangeCategoryMenu::populate(QMenu* menu, CategoryFactory& factory, +bool ModListChangeCategoryMenu::populate(QMenu* menu, CategoryFactory* factory, ModInfo::Ptr mod, int targetId) { const std::set& categories = mod->getCategories(); bool childEnabled = false; - for (unsigned int i = 1; i < factory.numCategories(); ++i) { - if (factory.getParentID(i) == targetId) { + for (unsigned int i = 1; i < factory->numCategories(); ++i) { + if (factory->getParentID(i) == targetId) { QMenu* targetMenu = menu; - if (factory.hasChildren(i)) { - targetMenu = menu->addMenu(factory.getCategoryName(i).replace('&', "&&")); + if (factory->hasChildren(i)) { + targetMenu = menu->addMenu(factory->getCategoryName(i).replace('&', "&&")); } - int id = factory.getCategoryID(i); + int id = factory->getCategoryID(i); QScopedPointer checkBox(new QCheckBox(targetMenu)); bool enabled = categories.find(id) != categories.end(); - checkBox->setText(factory.getCategoryName(i).replace('&', "&&")); + checkBox->setText(factory->getCategoryName(i).replace('&', "&&")); if (enabled) { childEnabled = true; } @@ -159,8 +162,8 @@ bool ModListChangeCategoryMenu::populate(QMenu* menu, CategoryFactory& factory, checkableAction->setData(id); targetMenu->addAction(checkableAction.take()); - if (factory.hasChildren(i)) { - if (populate(targetMenu, factory, mod, factory.getCategoryID(i)) || enabled) { + if (factory->hasChildren(i)) { + if (populate(targetMenu, factory, mod, factory->getCategoryID(i)) || enabled) { targetMenu->setIcon(QIcon(":/MO/gui/resources/check.png")); } } @@ -169,7 +172,7 @@ bool ModListChangeCategoryMenu::populate(QMenu* menu, CategoryFactory& factory, return childEnabled; } -ModListPrimaryCategoryMenu::ModListPrimaryCategoryMenu(CategoryFactory& categories, +ModListPrimaryCategoryMenu::ModListPrimaryCategoryMenu(CategoryFactory* categories, ModInfo::Ptr mod, QMenu* parent) : QMenu(tr("Primary Category"), parent) { @@ -178,17 +181,17 @@ ModListPrimaryCategoryMenu::ModListPrimaryCategoryMenu(CategoryFactory& categori }); } -void ModListPrimaryCategoryMenu::populate(const CategoryFactory& factory, +void ModListPrimaryCategoryMenu::populate(const CategoryFactory* factory, ModInfo::Ptr mod) { clear(); const std::set& categories = mod->getCategories(); for (int categoryID : categories) { - int catIdx = factory.getCategoryIndex(categoryID); + int catIdx = factory->getCategoryIndex(categoryID); QWidgetAction* action = new QWidgetAction(this); try { QRadioButton* categoryBox = - new QRadioButton(factory.getCategoryName(catIdx).replace('&', "&&"), this); + new QRadioButton(factory->getCategoryName(catIdx).replace('&', "&&"), this); categoryBox->setChecked(categoryID == mod->primaryCategory()); action->setDefaultWidget(categoryBox); action->setData(categoryID); @@ -216,7 +219,7 @@ int ModListPrimaryCategoryMenu::primaryCategory() const } ModListContextMenu::ModListContextMenu(const QModelIndex& index, OrganizerCore& core, - CategoryFactory& categories, ModListView* view) + CategoryFactory* categories, ModListView* view) : QMenu(view), m_core(core), m_categories(categories), m_index(index.model() == view->model() ? view->indexViewToModel(index) : index), m_view(view), m_actions(view->actions()) @@ -558,6 +561,12 @@ void ModListContextMenu::addRegularActions(ModInfo::Ptr mod) } } + if (mod->nexusId() > 0 && !mod->installationFile().isEmpty()) { + addAction(tr("Remap Category (From Nexus)"), [=]() { + m_actions.remapCategory(m_selected); + }); + } + if (mod->nexusId() > 0 && Settings::instance().nexus().trackedIntegration()) { switch (mod->trackedState()) { case TrackedState::TRACKED_FALSE: { diff --git a/src/modlistcontextmenu.h b/src/modlistcontextmenu.h index 320ece934..c2fe5bbfb 100644 --- a/src/modlistcontextmenu.h +++ b/src/modlistcontextmenu.h @@ -36,7 +36,7 @@ class ModListChangeCategoryMenu : public QMenu { Q_OBJECT public: - ModListChangeCategoryMenu(CategoryFactory& categories, ModInfo::Ptr mod, + ModListChangeCategoryMenu(CategoryFactory* categories, ModInfo::Ptr mod, QMenu* parent = nullptr); // return a list of pair from the menu @@ -47,7 +47,7 @@ class ModListChangeCategoryMenu : public QMenu // populate the tree with the category, using the enabled/disabled state from the // given mod // - bool populate(QMenu* menu, CategoryFactory& categories, ModInfo::Ptr mod, + bool populate(QMenu* menu, CategoryFactory* categories, ModInfo::Ptr mod, int targetId = 0); // internal implementation of categories() for recursion @@ -59,7 +59,7 @@ class ModListPrimaryCategoryMenu : public QMenu { Q_OBJECT public: - ModListPrimaryCategoryMenu(CategoryFactory& categories, ModInfo::Ptr mod, + ModListPrimaryCategoryMenu(CategoryFactory* categories, ModInfo::Ptr mod, QMenu* parent = nullptr); // return the selected primary category @@ -69,7 +69,7 @@ class ModListPrimaryCategoryMenu : public QMenu private: // populate the categories // - void populate(const CategoryFactory& categories, ModInfo::Ptr mod); + void populate(const CategoryFactory* categories, ModInfo::Ptr mod); }; class ModListContextMenu : public QMenu @@ -81,7 +81,7 @@ class ModListContextMenu : public QMenu // valid // ModListContextMenu(const QModelIndex& index, OrganizerCore& core, - CategoryFactory& categories, ModListView* modListView); + CategoryFactory* categories, ModListView* modListView); private: // adds the "Send to... " context menu @@ -105,7 +105,7 @@ class ModListContextMenu : public QMenu void addRegularActions(ModInfo::Ptr mod); OrganizerCore& m_core; - CategoryFactory& m_categories; + CategoryFactory* m_categories; QModelIndex m_index; QModelIndexList m_selected; ModListView* m_view; diff --git a/src/modlistview.cpp b/src/modlistview.cpp index 73bbd33df..944825fac 100644 --- a/src/modlistview.cpp +++ b/src/modlistview.cpp @@ -970,7 +970,7 @@ void ModListView::onCustomContextMenuRequested(const QPoint& pos) // no selection ModListGlobalContextMenu(*m_core, this).exec(viewport()->mapToGlobal(pos)); } else { - ModListContextMenu(contextIdx, *m_core, *m_categories, this) + ModListContextMenu(contextIdx, *m_core, m_categories, this) .exec(viewport()->mapToGlobal(pos)); } } catch (const std::exception& e) { diff --git a/src/modlistviewactions.cpp b/src/modlistviewactions.cpp index a6f0f07ee..cbac9c6c3 100644 --- a/src/modlistviewactions.cpp +++ b/src/modlistviewactions.cpp @@ -12,6 +12,7 @@ #include "categories.h" #include "csvbuilder.h" #include "directoryrefresher.h" +#include "downloadmanager.h" #include "filedialogmemory.h" #include "filterlist.h" #include "listdialog.h" @@ -259,6 +260,44 @@ void ModListViewActions::checkModsForUpdates() const } } +void ModListViewActions::assignCategories() const +{ + if (!GlobalSettings::hideAssignCategoriesQuestion()) { + QMessageBox warning; + warning.setWindowTitle(tr("Are you sure?")); + warning.setText( + tr("This action will remove any existing categories on any mod with a valid " + "Nexus category mapping. Are you certain you want to proceed?")); + warning.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); + QCheckBox dontShow(tr("&Don't show this again")); + warning.setCheckBox(&dontShow); + auto result = warning.exec(); + if (dontShow.isChecked()) + GlobalSettings::setHideAssignCategoriesQuestion(true); + if (result == QMessageBox::Cancel) + return; + } + for (auto mod : m_core.modList()->allMods()) { + ModInfo::Ptr modInfo = ModInfo::getByName(mod); + int nexusCategory = modInfo->getNexusCategory(); + if (!nexusCategory) { + QSettings downloadMeta(m_core.downloadsPath() + "/" + + modInfo->installationFile() + ".meta", + QSettings::IniFormat); + if (downloadMeta.contains("category")) { + nexusCategory = downloadMeta.value("category", 0).toInt(); + } + } + int newCategory = CategoryFactory::instance().resolveNexusID(nexusCategory); + if (newCategory != 0) { + for (auto category : modInfo->categories()) { + modInfo->removeCategory(category); + } + } + modInfo->setCategory(CategoryFactory::instance().getCategoryID(newCategory), true); + } +} + void ModListViewActions::checkModsForUpdates( std::multimap const& IDs) const { @@ -1082,6 +1121,27 @@ void ModListViewActions::willNotEndorsed(const QModelIndexList& indices) const } } +void ModListViewActions::remapCategory(const QModelIndexList& indices) const +{ + for (auto& idx : indices) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()); + + int categoryID = modInfo->getNexusCategory(); + if (!categoryID) { + QSettings downloadMeta(m_core.downloadsPath() + "/" + + modInfo->installationFile() + ".meta", + QSettings::IniFormat); + if (downloadMeta.contains("category")) { + categoryID = downloadMeta.value("category", 0).toInt(); + } + } + unsigned int categoryIndex = CategoryFactory::instance().resolveNexusID(categoryID); + if (categoryIndex != 0) + modInfo->setPrimaryCategory( + CategoryFactory::instance().getCategoryID(categoryIndex)); + } +} + void ModListViewActions::setColor(const QModelIndexList& indices, const QModelIndex& refIndex) const { diff --git a/src/modlistviewactions.h b/src/modlistviewactions.h index 5927654ff..ad20e7840 100644 --- a/src/modlistviewactions.h +++ b/src/modlistviewactions.h @@ -13,6 +13,7 @@ class MainWindow; class ModListView; class PluginListView; class OrganizerCore; +class DownloadManager; class ModListViewActions : public QObject { @@ -53,6 +54,10 @@ class ModListViewActions : public QObject void checkModsForUpdates() const; void checkModsForUpdates(const QModelIndexList& indices) const; + // auto-assign categories based on nexus ID + // + void assignCategories() const; + // start the "Export Mod List" dialog // void exportModListCSV() const; @@ -93,6 +98,7 @@ class ModListViewActions : public QObject void setTracked(const QModelIndexList& indices, bool tracked) const; void setEndorsed(const QModelIndexList& indices, bool endorsed) const; void willNotEndorsed(const QModelIndexList& indices) const; + void remapCategory(const QModelIndexList& indices) const; // set/reset color of the given selection, using the given reference index (index // at which the context menu was shown) diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 2c536b144..6a39128f4 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -91,6 +91,12 @@ void NexusBridge::requestToggleTracking(QString gameName, int modID, bool track, userData, m_SubModule)); } +void NexusBridge::requestGameInfo(QString gameName, QVariant userData) +{ + m_RequestIDs.insert( + m_Interface->requestGameInfo(gameName, this, userData, m_SubModule)); +} + void NexusBridge::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) @@ -194,6 +200,16 @@ void NexusBridge::nxmTrackingToggled(QString gameName, int modID, QVariant userD } } +void NexusBridge::nxmGameInfoAvailable(QString gameName, QVariant userData, + QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit gameInfoAvailable(gameName, userData, resultData); + } +} + void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, int errorCode, const QString& errorMessage) @@ -735,6 +751,31 @@ int NexusInterface::requestToggleTracking(QString gameName, int modID, bool trac return requestInfo.m_ID; } +int NexusInterface::requestGameInfo(QString gameName, QObject* receiver, + QVariant userData, const QString& subModule, + MOBase::IPluginGame const* game) +{ + if (m_User.shouldThrottle()) { + throttledWarning(m_User); + return -1; + } + + NXMRequestInfo requestInfo(NXMRequestInfo::TYPE_GAMEINFO, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmGameInfoAvailable(QString, QVariant, QVariant, int)), + receiver, SLOT(nxmGameInfoAvailable(QString, QVariant, QVariant, int)), + Qt::UniqueConnection); + + connect( + this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, int, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, QString)), + Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + int NexusInterface::requestInfoFromMd5(QString gameName, QByteArray& hash, QObject* receiver, QVariant userData, const QString& subModule, @@ -928,6 +969,9 @@ void NexusInterface::nextRequest() .arg(info.m_URL) .arg(info.m_GameName) .arg(QString(info.m_Hash.toHex())); + } break; + case NXMRequestInfo::TYPE_GAMEINFO: { + url = QStringLiteral("%1/games/%2").arg(info.m_URL).arg(info.m_GameName); } } } else { @@ -1099,6 +1143,10 @@ void NexusInterface::requestFinished(std::list::iterator iter) emit nxmFileInfoFromMd5Available(iter->m_GameName, iter->m_UserData, result, iter->m_ID); } break; + case NXMRequestInfo::TYPE_GAMEINFO: { + emit nxmGameInfoAvailable(iter->m_GameName, iter->m_UserData, result, + iter->m_ID); + } break; } m_User.limits(parseLimits(reply)); @@ -1201,6 +1249,17 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo( m_Endorse(false), m_Track(false), m_Hash(QByteArray()) {} +NexusInterface::NXMRequestInfo::NXMRequestInfo(Type type, QVariant userData, + const QString& subModule, + MOBase::IPluginGame const* game) + : m_ModID(0), m_ModVersion("0"), m_FileID(0), m_Reply(nullptr), m_Type(type), + m_UpdatePeriod(UpdatePeriod::NONE), m_UserData(userData), m_Timeout(nullptr), + m_Reroute(false), m_ID(s_NextID.fetchAndAddAcquire(1)), + m_URL(get_management_url()), m_SubModule(subModule), + m_NexusGameID(game->nexusGameID()), m_GameName(game->gameNexusName()), + m_Endorse(false), m_Track(false), m_Hash(QByteArray()) +{} + NexusInterface::NXMRequestInfo::NXMRequestInfo( int modID, int fileID, NexusInterface::NXMRequestInfo::Type type, QVariant userData, const QString& subModule, MOBase::IPluginGame const* game) diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 331ce55d4..5fab222f1 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -113,6 +113,12 @@ class NexusBridge : public MOBase::IModRepositoryBridge virtual void requestToggleTracking(QString gameName, int modID, bool track, QVariant userData); + /** + * @brief requestGameInfo + * @param userData user data to be returned with the result + */ + virtual void requestGameInfo(QString gameName, QVariant userData); + public slots: void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, @@ -129,6 +135,8 @@ public slots: void nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int requestID); void nxmTrackingToggled(QString gameName, int modID, QVariant userData, bool tracked, int requestID); + void nxmGameInfoAvailable(QString gameName, QVariant userData, QVariant resultData, + int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, int errorCode, const QString& errorMessage); @@ -444,6 +452,37 @@ class NexusInterface : public QObject QVariant userData, const QString& subModule, MOBase::IPluginGame const* game); + /** + * @param gameName the game short name to support multiple game sources + * @brief toggle tracking state of the mod + * @param modID id of the mod + * @param track true if the mod should be tracked, false for not tracked + * @param receiver the object to receive the result asynchronously via a signal + * (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @param game the game with which the mods are associated + * @return int an id to identify the request + */ + int requestGameInfo(QString gameName, QObject* receiver, QVariant userData, + const QString& subModule) + { + return requestGameInfo(gameName, receiver, userData, subModule, getGame(gameName)); + } + + /** + * @param gameName the game short name to support multiple game sources + * @brief toggle tracking state of the mod + * @param modID id of the mod + * @param track true if the mod should be tracked, false for not tracked + * @param receiver the object to receive the result asynchronously via a signal + * (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @param game the game with which the mods are associated + * @return int an id to identify the request + */ + int requestGameInfo(QString gameName, QObject* receiver, QVariant userData, + const QString& subModule, MOBase::IPluginGame const* game); + /** * */ @@ -552,6 +591,8 @@ class NexusInterface : public QObject void nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int requestID); void nxmTrackingToggled(QString gameName, int modID, QVariant userData, bool tracked, int requestID); + void nxmGameInfoAvailable(QString gameName, QVariant userData, QVariant resultData, + int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, int errorCode, const QString& errorString); void requestsChanged(const APIStats& stats, const APIUserAccount& user); @@ -592,6 +633,7 @@ private slots: TYPE_TOGGLETRACKING, TYPE_TRACKEDMODS, TYPE_FILEINFO_MD5, + TYPE_GAMEINFO, } m_Type; UpdatePeriod m_UpdatePeriod; QVariant m_UserData; @@ -614,6 +656,8 @@ private slots: const QString& subModule, MOBase::IPluginGame const* game); NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString& subModule, MOBase::IPluginGame const* game); + NXMRequestInfo(Type type, QVariant userData, const QString& subModule, + MOBase::IPluginGame const* game); NXMRequestInfo(Type type, QVariant userData, const QString& subModule); NXMRequestInfo(UpdatePeriod period, Type type, QVariant userData, const QString& subModule, MOBase::IPluginGame const* game); diff --git a/src/organizercore.cpp b/src/organizercore.cpp index e3d1ff293..b4e758d1b 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -1,4 +1,5 @@ #include "organizercore.h" +#include "categoriesdialog.h" #include "credentialsdialog.h" #include "delayedfilewriter.h" #include "directoryrefresher.h" @@ -759,8 +760,9 @@ OrganizerCore::doInstall(const QString& archivePath, GuessedValue modNa // this prevents issue with third-party plugins, e.g., if the installed mod is // activated before the structure is ready // - // we need to fetch modIndex() within the call back because the index is only valid - // after the call to refresh(), but we do not want to connect after refresh() + // we need to fetch modIndex() within the call back because the index is only + // valid after the call to refresh(), but we do not want to connect after + // refresh() // connect( this, &OrganizerCore::directoryStructureReady, this, @@ -801,16 +803,26 @@ OrganizerCore::doInstall(const QString& archivePath, GuessedValue modNa emit modInstalled(modName); return {modIndex, modInfo}; } else { - m_InstallationManager.notifyInstallationEnd(result, nullptr); - if (m_InstallationManager.wasCancelled()) { - QMessageBox::information( - qApp->activeWindow(), tr("Extraction cancelled"), - tr("The installation was cancelled while extracting files. " - "If this was prior to a FOMOD setup, this warning may be ignored. " - "However, if this was during installation, the mod will likely be missing " - "files."), - QMessageBox::Ok); - refresh(); + if (result.result() == MOBase::IPluginInstaller::RESULT_CATEGORYREQUESTED) { + CategoriesDialog dialog(qApp->activeWindow()); + + if (dialog.exec() == QDialog::Accepted) { + dialog.commitChanges(); + refresh(); + } + } else { + m_InstallationManager.notifyInstallationEnd(result, nullptr); + if (m_InstallationManager.wasCancelled()) { + QMessageBox::information( + qApp->activeWindow(), tr("Extraction cancelled"), + tr("The installation was cancelled while extracting files. " + "If this was prior to a FOMOD setup, this warning may be ignored. " + "However, if this was during installation, the mod will likely be " + "missing " + "files."), + QMessageBox::Ok); + refresh(); + } } } diff --git a/src/settings.cpp b/src/settings.cpp index b568d8028..04a094676 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1936,6 +1936,16 @@ void NexusSettings::setTrackedIntegration(bool b) const set(m_Settings, "Settings", "tracked_integration", b); } +bool NexusSettings::categoryMappings() const +{ + return get(m_Settings, "Settings", "category_mappings", true); +} + +void NexusSettings::setCategoryMappings(bool b) const +{ + set(m_Settings, "Settings", "category_mappings", b); +} + void NexusSettings::registerAsNXMHandler(bool force) { const auto nxmPath = QCoreApplication::applicationDirPath() + "/" + @@ -2430,6 +2440,26 @@ void GlobalSettings::setHideTutorialQuestion(bool b) settings().setValue("HideTutorialQuestion", b); } +bool GlobalSettings::hideCategoryReminder() +{ + return settings().value("HideCategoryReminder", false).toBool(); +} + +void GlobalSettings::setHideCategoryReminder(bool b) +{ + settings().setValue("HideCategoryReminder", b); +} + +bool GlobalSettings::hideAssignCategoriesQuestion() +{ + return settings().value("HideAssignCategoriesQuestion", false).toBool(); +} + +void GlobalSettings::setHideAssignCategoriesQuestion(bool b) +{ + settings().setValue("HideAssignCategoriesQuestion", b); +} + bool GlobalSettings::nexusApiKey(QString& apiKey) { QString tempKey = getWindowsCredential("APIKEY"); diff --git a/src/settings.h b/src/settings.h index 34a8669af..30254c066 100644 --- a/src/settings.h +++ b/src/settings.h @@ -514,6 +514,11 @@ class NexusSettings bool trackedIntegration() const; void setTrackedIntegration(bool b) const; + // returns whether nexus category mappings are enabled + // + bool categoryMappings() const; + void setCategoryMappings(bool b) const; + // registers MO as the handler for nxm links // // if 'force' is true, the registration dialog will be shown even if the user @@ -923,6 +928,12 @@ class GlobalSettings static bool hideTutorialQuestion(); static void setHideTutorialQuestion(bool b); + static bool hideCategoryReminder(); + static void setHideCategoryReminder(bool b); + + static bool hideAssignCategoriesQuestion(); + static void setHideAssignCategoriesQuestion(bool b); + // if the key exists from the credentials store, puts it in `apiKey` and // returns true; otherwise, returns false and leaves `apiKey` untouched // diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 27949c3aa..3ed66456e 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -17,7 +17,7 @@ - 0 + 4 @@ -1340,6 +1340,16 @@ If you disable this feature, MO will only display official DLCs this way. Please + + + + Use Nexus category mappings + + + true + + + diff --git a/src/settingsdialoggeneral.h b/src/settingsdialoggeneral.h index 619381905..6c1ee8670 100644 --- a/src/settingsdialoggeneral.h +++ b/src/settingsdialoggeneral.h @@ -1,6 +1,7 @@ #ifndef SETTINGSDIALOGGENERAL_H #define SETTINGSDIALOGGENERAL_H +#include "plugincontainer.h" #include "settings.h" #include "settingsdialog.h" diff --git a/src/settingsdialognexus.cpp b/src/settingsdialognexus.cpp index 2e706e42c..abd487f14 100644 --- a/src/settingsdialognexus.cpp +++ b/src/settingsdialognexus.cpp @@ -294,6 +294,7 @@ NexusSettingsTab::NexusSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab { ui->endorsementBox->setChecked(settings().nexus().endorsementIntegration()); ui->trackedBox->setChecked(settings().nexus().trackedIntegration()); + ui->categoryMappingsBox->setChecked(settings().nexus().categoryMappings()); ui->hideAPICounterBox->setChecked(settings().interface().hideAPICounter()); // display server preferences @@ -359,6 +360,7 @@ void NexusSettingsTab::update() { settings().nexus().setEndorsementIntegration(ui->endorsementBox->isChecked()); settings().nexus().setTrackedIntegration(ui->trackedBox->isChecked()); + settings().nexus().setCategoryMappings(ui->categoryMappingsBox->isChecked()); settings().interface().setHideAPICounter(ui->hideAPICounterBox->isChecked()); auto servers = settings().network().servers();