diff options
| author | Jeremy Rimpo <jeremy.rimpo@servermonkey.com> | 2023-09-25 17:39:41 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-09-25 17:39:41 -0500 |
| commit | 79d457f598bcfa7a8d094a87033a31f370c9c4c1 (patch) | |
| tree | 3674e6b1223ac104422ce77b06d020c50ad31cc0 /src | |
| parent | 70509275405d2a5c5e94743786f03b17ef2051f2 (diff) | |
| parent | 74668b9363d877d055a5e8b8c7d001ba448dc832 (diff) | |
Merge pull request #1880 from ModOrganizer2/qt6-category
WIP: Category Revamp
Diffstat (limited to 'src')
39 files changed, 1500 insertions, 255 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 500b0ca5..1b9a0f85 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 70efd706..c1aa756f 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,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<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 +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<NexusCategory>(), 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<QByteArray> nexCells = nexLine.split('|'); + if (nexCells.count() == 3) { + 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 {}").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<NexusCategory>(), 0); } void CategoryFactory::setParents() { - for (std::vector<Category>::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<Category>::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<int, unsigned int>::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<Category>::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<bool(const Category& category)> f return result; } -int CategoryFactory::addCategory(const QString& name, const std::vector<int>& nexusIDs, +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, std::vector<NexusCategory>())); + m_IDMap[id] = index; +} + +void CategoryFactory::addCategory(int id, const QString& name, + const std::vector<NexusCategory>& 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<int>(m_Categories.size()); + m_Categories.push_back(Category(index, id, name, parentID, nexusCats)); m_IDMap[id] = index; } +void CategoryFactory::setNexusCategories( + const std::vector<CategoryFactory::NexusCategory>& 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<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; } @@ -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<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; - } 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 b938bd19..15b9bbc1 100644 --- a/src/categories.h +++ b/src/categories.h @@ -25,14 +25,17 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <map> #include <vector> +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 @@ public: }; 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<int>& nexusIDs, int parentID) + Category(int sortValue, int id, const QString name, int parentID, + std::vector<NexusCategory> 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<int> 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<NexusCategory> m_NexusCats; + bool m_HasChildren; }; public: @@ -89,7 +132,12 @@ public: **/ void saveCategories(); - int addCategory(const QString& name, const std::vector<int>& nexusIDs, int parentID); + void setNexusCategories(const std::vector<CategoryFactory::NexusCategory>& nexusCats); + + void refreshNexusCategories(CategoriesDialog* dialog); + + int addCategory(const QString& name, const std::vector<NexusCategory>& nexusCats, + int parentID); /** * @brief retrieve the number of available categories @@ -190,13 +238,23 @@ public: */ 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<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 +265,7 @@ private: std::vector<Category> m_Categories; std::map<int, unsigned int> m_IDMap; - std::map<int, unsigned int> m_NexusMap; + std::map<int, NexusCategory> m_NexusMap; private: // called by isDescendantOf() diff --git a/src/categoriesdialog.cpp b/src/categoriesdialog.cpp index 99a931ef..ff226286 100644 --- a/src/categoriesdialog.cpp +++ b/src/categoriesdialog.cpp @@ -19,6 +19,9 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #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<int> 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<CategoryFactory::NexusCategory> 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<CategoryFactory::NexusCategory> 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<CategoryFactory::Category>::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<QTableWidgetItem> idItem(new QTableWidgetItem()); - idItem->setData(Qt::DisplayRole, category.m_ID); + idItem->setData(Qt::DisplayRole, category.ID()); - QScopedPointer<QTableWidgetItem> nameItem(new QTableWidgetItem(category.m_Name)); - QScopedPointer<QTableWidgetItem> nexusIDItem( - new QTableWidgetItem(MOBase::VectorJoin(category.m_NexusIDs, ","))); + QScopedPointer<QTableWidgetItem> nameItem(new QTableWidgetItem(category.name())); QScopedPointer<QTableWidgetItem> parentIDItem(new QTableWidgetItem()); - parentIDItem->setData(Qt::DisplayRole, category.m_ParentID); + parentIDItem->setData(Qt::DisplayRole, category.parentID()); + QScopedPointer<QTableWidgetItem> 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<QListWidgetItem> 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<QTableWidgetItem> 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<QTableWidgetItem> idItem(new QTableWidgetItem()); + idItem->setData(Qt::DisplayRole, ++m_HighestID); + + QScopedPointer<QTableWidgetItem> nameItem(new QTableWidgetItem(name)); + QScopedPointer<QTableWidgetItem> 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<QTableWidgetItem> 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<QListWidgetItem> 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 2ac84124..94f390b0 100644 --- a/src/categoriesdialog.h +++ b/src/categoriesdialog.h @@ -20,6 +20,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #ifndef CATEGORIESDIALOG_H #define CATEGORIESDIALOG_H +#include "categories.h" +#include "plugincontainer.h" #include "tutorabledialog.h" #include <set> @@ -49,11 +51,18 @@ public: **/ 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: private: Ui::CategoriesDialog* ui; + PluginContainer* m_PluginContainer; int m_ContextRow; int m_HighestID; std::set<int> m_IDs; + std::vector<CategoryFactory::NexusCategory> m_NexusCategories; }; #endif // CATEGORIESDIALOG_H diff --git a/src/categoriesdialog.ui b/src/categoriesdialog.ui index 582a7608..4f96d8a6 100644 --- a/src/categoriesdialog.ui +++ b/src/categoriesdialog.ui @@ -6,19 +6,58 @@ <rect>
<x>0</x>
<y>0</y>
- <width>553</width>
- <height>444</height>
+ <width>711</width>
+ <height>434</height>
</rect>
</property>
<property name="windowTitle">
<string>Categories</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="QTableWidget" name="categoriesTable">
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="4" column="3">
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="5">
+ <widget class="QPushButton" name="nexusRefresh">
+ <property name="text">
+ <string>Refresh from Nexus</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="4">
+ <widget class="QPushButton" name="nexusImportButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string><-- Import Nexus Cats</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="3">
+ <widget class="CategoriesTable" name="categoriesTable">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
<property name="dragEnabled">
<bool>false</bool>
</property>
@@ -26,7 +65,7 @@ <bool>false</bool>
</property>
<property name="dragDropMode">
- <enum>QAbstractItemView::NoDragDrop</enum>
+ <enum>QAbstractItemView::DropOnly</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::IgnoreAction</enum>
@@ -40,21 +79,24 @@ <property name="gridStyle">
<enum>Qt::DashLine</enum>
</property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
<property name="wordWrap">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
+ <attribute name="horizontalHeaderMinimumSectionSize">
+ <number>26</number>
+ </attribute>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>100</number>
</attribute>
<attribute name="horizontalHeaderHighlightSections">
<bool>true</bool>
</attribute>
- <attribute name="horizontalHeaderMinimumSectionSize">
- <number>26</number>
- </attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>false</bool>
</attribute>
@@ -77,51 +119,93 @@ <string>Name</string>
</property>
<property name="toolTip">
- <string>Name of the Categorie used for display.</string>
+ <string>The display name of the category.</string>
</property>
<property name="whatsThis">
- <string>Name of the Categorie used for display.</string>
+ <string>The display name of the category.</string>
</property>
</column>
<column>
<property name="text">
- <string>Nexus IDs</string>
+ <string>Parent ID</string>
</property>
<property name="toolTip">
- <string>Comma-Separated list of Nexus IDs to be matched to the internal ID.</string>
- </property>
- <property name="whatsThis">
- <string><!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></string>
+ <string>If set, the category is defined as a sub-category of another one. Parent ID needs to be a valid category ID.</string>
</property>
</column>
<column>
<property name="text">
- <string>Parent ID</string>
+ <string>Nexus Categories</string>
</property>
<property name="toolTip">
- <string>If set, the category is defined as a sub-category of another one. Parent ID needs to be a valid category ID.</string>
+ <string>Comma-Separated list of Nexus IDs to be matched to the internal ID.</string>
+ </property>
+ <property name="whatsThis">
+ <string>
+ <!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>
+ </string>
</property>
</column>
</widget>
</item>
- <item>
- <widget class="QDialogButtonBox" name="buttonBox">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
+ <item row="0" column="4" colspan="2">
+ <widget class="QGroupBox" name="groupBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
</property>
- <property name="standardButtons">
- <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="toolTip">
+ <string>Drag & drop nexus categories from this pane onto the target category on the left.</string>
+ </property>
+ <property name="layoutDirection">
+ <enum>Qt::LeftToRight</enum>
+ </property>
+ <property name="title">
+ <string>Nexus Categories (Drag && Drop to Assign)</string>
</property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QListWidget" name="nexusCategoryList">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="dragEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DragOnly</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
</widget>
</item>
</layout>
</widget>
+ <customwidgets>
+ <customwidget>
+ <class>CategoriesTable</class>
+ <extends>QTableWidget</extends>
+ <header>categoriestable.h</header>
+ </customwidget>
+ </customwidgets>
<resources/>
<connections>
<connection>
diff --git a/src/categoriestable.cpp b/src/categoriestable.cpp new file mode 100644 index 00000000..fc53fb58 --- /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 <http://www.gnu.org/licenses/>. +*/ + +#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<int, QVariant> 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 00000000..8ec797de --- /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 <http://www.gnu.org/licenses/>. +*/ + +#ifndef CATEGORIESTABLE_H +#define CATEGORIESTABLE_H + +#include <QTableWidget> +#include <log.h> + +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 00000000..4b09c391 --- /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 00000000..bfc2eee4 --- /dev/null +++ b/src/categoryimportdialog.h @@ -0,0 +1,44 @@ +#ifndef CATEGORYIMPORTDIALOG_H +#define CATEGORYIMPORTDIALOG_H + +#include <QDialog> + +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 00000000..c3a18ab1 --- /dev/null +++ b/src/categoryimportdialog.ui @@ -0,0 +1,144 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CategoryImportDialog</class> + <widget class="QDialog" name="CategoryImportDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>193</height> + </rect> + </property> + <property name="windowTitle"> + <string>Nexus Category Import</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string><h3>How do you want to import the categories?</h3></string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Import Strategy</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QRadioButton" name="mergeOption"> + <property name="text"> + <string>Merge</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">strategyGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="replaceOption"> + <property name="text"> + <string>Replace</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + <attribute name="buttonGroup"> + <string notr="true">strategyGroup</string> + </attribute> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Options</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QCheckBox" name="assignOption"> + <property name="text"> + <string>Assign nexus mappings</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="remapOption"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Remap existing mappings</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> + <buttongroups> + <buttongroup name="strategyGroup"/> + </buttongroups> +</ui> diff --git a/src/filterlist.cpp b/src/filterlist.cpp index d0c7199e..3be67def 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 <utility.h> diff --git a/src/installationmanager.cpp b/src/installationmanager.cpp index 8c2ce78d..37c86c7e 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 ebe7da98..ce2c4dd0 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<CategoryFactory::NexusCategory> 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 8cd55c90..99feca83 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -317,6 +317,8 @@ private: std::atomic<bool> 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 1d30ac95..1028c66f 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<int>(), 0); + id = CategoryFactory::instance().addCategory( + categoryName, std::vector<CategoryFactory::NexusCategory>(), 0); } setCategory(id, true); } diff --git a/src/modinfo.h b/src/modinfo.h index 9895d44c..d9e87ae0 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -888,6 +888,16 @@ public: // Nexus stuff */ 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 01abf184..0c3c29d7 100644 --- a/src/modinfobackup.h +++ b/src/modinfobackup.h @@ -38,6 +38,8 @@ public: 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 269aebe3..53655aa5 100644 --- a/src/modinfodialog.ui +++ b/src/modinfodialog.ui @@ -966,7 +966,7 @@ text-align: left;</string> <number>0</number>
</property>
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,0,0,0,0,0,1">
+ <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,0,0,0,0,0,0,0,1">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
@@ -1027,6 +1027,29 @@ p, li { white-space: pre-wrap; } </widget>
</item>
<item>
+ <widget class="QLabel" name="categoryLabel">
+ <property name="text">
+ <string>Category</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="category">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>1</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
diff --git a/src/modinfodialognexus.cpp b/src/modinfodialognexus.cpp index 56636145..485f138a 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 e9a8720f..e752018a 100644 --- a/src/modinfodialognexus.h +++ b/src/modinfodialognexus.h @@ -57,6 +57,7 @@ private: void onModIDChanged(); void onSourceGameChanged(); void onVersionChanged(); + void onCategoryChanged(); void onRefreshBrowser(); void onVisitNexus(); diff --git a/src/modinfoforeign.h b/src/modinfoforeign.h index 2c956fb7..dc66e1b0 100644 --- a/src/modinfoforeign.h +++ b/src/modinfoforeign.h @@ -60,6 +60,8 @@ public: 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 02154902..45b5bbdd 100644 --- a/src/modinfooverwrite.h +++ b/src/modinfooverwrite.h @@ -68,6 +68,8 @@ public: 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<std::pair<int, int>> installedFiles() const override { return {}; } diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 4c1004e6..c79fc574 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<QColor>(); - 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<QColor>(); + m_TrackedState = metaFile.value("tracked", false).toBool() + ? TrackedState::TRACKED_TRUE + : TrackedState::TRACKED_FALSE; if (metaFile.contains("endorsed")) { if (metaFile.value("endorsed").canConvert<int>()) { using ut = std::underlying_type_t<EndorsedState>; @@ -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 6c85d66c..a408d966 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -379,6 +379,16 @@ public: */ 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: 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 c34dd92b..67bc3d56 100644 --- a/src/modinfoseparator.h +++ b/src/modinfoseparator.h @@ -45,6 +45,8 @@ public: 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 e88cadc8..06cd19d6 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<int>& 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<QCheckBox> 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<int>& 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 320ece93..c2fe5bbf 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 <category id, enabled> from the menu @@ -47,7 +47,7 @@ private: // 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 @@ public: 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 @@ public: // 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 @@ private: 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 73bbd33d..944825fa 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 a6f0f07e..cbac9c6c 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<QString, int> 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 5927654f..ad20e784 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 @@ public: 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 @@ public: 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 2c536b14..6a39128f 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<int>::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<NXMRequestInfo>::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 331ce55d..5fab222f 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -113,6 +113,12 @@ public: 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); @@ -445,6 +453,37 @@ public: 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); + + /** * */ int requestInfoFromMd5(QString gameName, QByteArray& hash, QObject* receiver, @@ -552,6 +591,8 @@ signals: 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: TYPE_TOGGLETRACKING, TYPE_TRACKEDMODS, TYPE_FILEINFO_MD5, + TYPE_GAMEINFO, } m_Type; UpdatePeriod m_UpdatePeriod; QVariant m_UserData; @@ -614,6 +656,8 @@ private: 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 e3d1ff29..b4e758d1 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<QString> 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<QString> 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 b568d802..04a09467 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<bool>(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 34a8669a..30254c06 100644 --- a/src/settings.h +++ b/src/settings.h @@ -514,6 +514,11 @@ public: 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 @@ public: 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 27949c3a..3ed66456 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -17,7 +17,7 @@ <item> <widget class="QTabWidget" name="tabWidget"> <property name="currentIndex"> - <number>0</number> + <number>4</number> </property> <widget class="QWidget" name="generalTab"> <attribute name="title"> @@ -1341,6 +1341,16 @@ If you disable this feature, MO will only display official DLCs this way. Please </widget> </item> <item> + <widget class="QCheckBox" name="categoryMappingsBox"> + <property name="text"> + <string>Use Nexus category mappings</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> <widget class="QCheckBox" name="hideAPICounterBox"> <property name="toolTip"> <string><html><head/><body><p>By default, a counter is displayed in the bottom right corner. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.</p></body></html></string> diff --git a/src/settingsdialoggeneral.h b/src/settingsdialoggeneral.h index 61938190..6c1ee867 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 2e706e42..abd487f1 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(); |
