From f07f6b8904f69a92cd24e3c52eaf1fc6db8e60dd Mon Sep 17 00:00:00 2001 From: Silarn Date: Tue, 29 Jan 2019 20:10:53 -0600 Subject: Update Process Improvements * 5 minute batch auto-update of up to 10 mods - Still able to force an update of all 'unchecked' mods - Prioritizes mods with oldest update 'age' * Implemented 1-hour wait between update checks per mod * Fixed issues with update progress display * Only enable update filter automatically if 'force update' * Improved display of version update status in mod list - Italic text when ready to perform update check - Tooltip indicates when next update is available * Fixed remaining issues with update fallback check * Only trigger one update API request for duplicate sources --- src/mainwindow.cpp | 80 +- src/mainwindow.h | 3 + src/modinfo.cpp | 49 +- src/modinfo.h | 1433 ++++++++++++------------ src/modinfodialog.cpp | 2 +- src/modinfoforeign.h | 3 + src/modinfooverwrite.h | 3 + src/modinforegular.cpp | 42 +- src/modinforegular.h | 18 +- src/modinfoseparator.h | 4 +- src/modlist.cpp | 2808 ++++++++++++++++++++++++------------------------ 11 files changed, 2288 insertions(+), 2157 deletions(-) (limited to 'src') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 094a44af..b57c62f8 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -400,7 +400,7 @@ MainWindow::MainWindow(QSettings &initSettings connect(NexusInterface::instance(&pluginContainer), SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), this, SLOT(nxmDownloadURLs(QString,int,int,QVariant,QVariant,int))); connect(NexusInterface::instance(&pluginContainer), SIGNAL(needLogin()), &m_OrganizerCore, SLOT(nexusApi())); connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(validateFailed(QString)), this, SLOT(validationFailed(QString))); - connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool)), + connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool, std::tuple)), this, SLOT(updateWindowTitle(const QString&, bool))); connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool, std::tuple)), NexusInterface::instance(&m_PluginContainer), SLOT(setRateMax(const QString&, bool, std::tuple))); @@ -439,6 +439,10 @@ MainWindow::MainWindow(QSettings &initSettings connect(&m_SaveMetaTimer, SIGNAL(timeout()), this, SLOT(saveModMetas())); m_SaveMetaTimer.start(5000); + m_ModUpdateTimer.setSingleShot(false); + connect(&m_ModUpdateTimer, SIGNAL(timeout()), this, SLOT(modUpdateCheck())); + m_ModUpdateTimer.start(300 * 1000); + setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool()); FileDialogMemory::restore(initSettings); @@ -491,6 +495,8 @@ MainWindow::MainWindow(QSettings &initSettings updatePluginCount(); updateModCount(); + + modUpdateCheck(); } @@ -3996,6 +4002,14 @@ void MainWindow::checkModsForUpdates() m_ModsToUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); } } + + m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); + for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { + if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { + ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); + break; + } + } } void MainWindow::changeVersioningScheme() { @@ -4399,7 +4413,7 @@ void MainWindow::initModListContextMenu(QMenu *menu) menu->addAction(tr("Enable all visible"), this, SLOT(enableVisibleMods())); menu->addAction(tr("Disable all visible"), this, SLOT(disableVisibleMods())); - menu->addAction(tr("Check all for update"), this, SLOT(checkModsForUpdates())); + menu->addAction(tr("Force update check"), this, SLOT(checkModsForUpdates())); menu->addAction(tr("Refresh"), &m_OrganizerCore, SLOT(profileRefresh())); menu->addAction(tr("Export to csv..."), this, SLOT(exportModListCSV())); } @@ -5427,21 +5441,32 @@ void MainWindow::updateDownloadView() void MainWindow::modDetailsUpdated(bool) { - if (--m_ModsToUpdate == 0) { + if (m_ModsToUpdate <= 0) { statusBar()->hide(); - m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); - for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { - if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { - ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); - break; - } - } m_RefreshProgress->setVisible(false); } else { m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); } } +void MainWindow::modUpdateCheck() +{ + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + m_ModsToUpdate += ModInfo::autoUpdateCheck(&m_PluginContainer, this); + m_RefreshProgress->setRange(0, m_ModsToUpdate); + } else { + QString apiKey; + if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { + m_OrganizerCore.doAfterLogin([this]() { this->checkModsForUpdates(); }); + NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + } else { // otherwise there will be no endorsement info + MessageDialog::showMessage(tr("Not logged in, endorsement information will be wrong"), + this, true); + m_ModsToUpdate += ModInfo::autoUpdateCheck(&m_PluginContainer, this); + } + } +} + void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) { QVariantMap resultInfo = resultData.toMap(); @@ -5458,12 +5483,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD } } std::vector modsList = ModInfo::getByModID(gameNameReal, modID); - // Not clear to me what this is accomplishing? - //if (sameNexus) { - // std::vector mainInfo = ModInfo::getByModID(m_OrganizerCore.managedGame()->gameShortName(), modID); - // info.reserve(info.size() + mainInfo.size()); - // info.insert(info.end(), mainInfo.begin(), mainInfo.end()); - //} + for (auto mod : modsList) { QString installedFile = mod->getInstallationFile(); for (auto update : fileUpdates) { @@ -5511,6 +5531,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD if (foundUpdate) { // Just get the standard data updates for endorsements and descriptions + mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); mod->updateNXMInfo(); } else { // Scrape mod data here so we can use the mod version if no file update was located @@ -5518,29 +5539,9 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD } } - // Old endorsement and mod info updater - //for - // if (updateData["old_file_id"].toInt() == mod->readMeta()) - // (*iter)->setNewestVersion(result["version"].toString()); - //(*iter)->setNexusDescription(result["description"].toString()); - //if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated() && - // result.contains("voted_by_user") && - // Settings::instance().endorsementIntegration()) { - // // don't use endorsement info if we're not logged in or if the response doesn't contain it - // (*iter)->setIsEndorsed(result["voted_by_user"].toBool()); - //} - - if (m_ModsToUpdate <= 0) { + if (--m_ModsToUpdate <= 0) { statusBar()->hide(); - m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); - for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { - if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { - ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); - break; - } - } - } - else { + } else { m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); } } @@ -5570,9 +5571,8 @@ void MainWindow::nxmDescriptionAvailable(QString gameName, int modID, QVariant u else mod->setIsEndorsed(false); } + mod->saveMeta(); } - disconnect(sender(), SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), - this, SLOT(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int))); } void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int) diff --git a/src/mainwindow.h b/src/mainwindow.h index b3490d07..65ca99dd 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -348,6 +348,7 @@ private: QTimer m_CheckBSATimer; QTimer m_SaveMetaTimer; QTimer m_UpdateProblemsTimer; + QTimer m_ModUpdateTimer; QFuture m_MetaSave; @@ -508,6 +509,8 @@ private slots: void modInstalled(const QString &modName); + void modUpdateCheck(); + void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int); diff --git a/src/modinfo.cpp b/src/modinfo.cpp index e7b7657b..5d5cb57e 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -295,20 +295,63 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv // NexusInterface::instance(pluginContainer)->requestUpdates(game->nexusModOrganizerID(), receiver, QVariant(), game->gameShortName(), QString()); //} - std::multimap> organizedGames; + std::multimap organizedGames; for (auto mod : s_Collection) { if (mod->canBeUpdated()) { - organizedGames.insert(std::pair>(mod->getGameName(), mod)); + organizedGames.insert(std::make_pair(mod->getGameName(), mod->getNexusID())); } } + result = organizedGames.size(); + for (auto game : organizedGames) { - NexusInterface::instance(pluginContainer)->requestUpdates(game.second->getNexusID(), receiver, QVariant(), game.first, QString()); + NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); + } + + return result; +} + + +int ModInfo::autoUpdateCheck(PluginContainer *pluginContainer, QObject *receiver) +{ + qInfo("Initializing periodic update check."); + int result = 0; + + std::vector> sortedMods; + std::multimap organizedGames; + for (auto mod : s_Collection) { + if (mod->canBeUpdated()) { + sortedMods.push_back(mod); + } + } + + std::sort(sortedMods.begin(), sortedMods.end(), [](QSharedPointer a, QSharedPointer b) -> bool { + return a->getLastNexusUpdate() > b->getLastNexusUpdate(); + }); + + if (sortedMods.size() > 10) + sortedMods.resize(10); + + result = sortedMods.size(); + + if (sortedMods.size()) { + qInfo("Checking updates for %d mods...", sortedMods.size()); + + for (auto mod : sortedMods) { + organizedGames.insert(std::make_pair(mod->getGameName(), mod->getNexusID())); + } + + for (auto game : organizedGames) { + NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); + } + } else { + qInfo("No mods require updates at this time."); } return result; } + void ModInfo::setVersion(const VersionInfo &version) { m_Version = version; diff --git a/src/modinfo.h b/src/modinfo.h index 6b7c714e..9c753752 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -1,710 +1,723 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef MODINFO_H -#define MODINFO_H - -#include "imodinterface.h" -#include "versioninfo.h" - -class PluginContainer; - -class QDateTime; -class QDir; -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -namespace MOBase { class IPluginGame; } -namespace MOShared { class DirectoryEntry; } - -/** - * @brief Represents meta information about a single mod. - * - * Represents meta information about a single mod. The class interface is used - * to manage the mod collection - * - **/ -class ModInfo : public QObject, public MOBase::IModInterface -{ - - Q_OBJECT - -public: - - typedef QSharedPointer Ptr; - - static QString s_HiddenExt; - - enum EFlag { - FLAG_INVALID, - FLAG_BACKUP, - FLAG_SEPARATOR, - FLAG_OVERWRITE, - FLAG_FOREIGN, - FLAG_NOTENDORSED, - FLAG_NOTES, - FLAG_CONFLICT_OVERWRITE, - FLAG_CONFLICT_OVERWRITTEN, - FLAG_CONFLICT_MIXED, - FLAG_CONFLICT_REDUNDANT, - FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE, - FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN, - FLAG_ARCHIVE_CONFLICT_OVERWRITE, - FLAG_ARCHIVE_CONFLICT_OVERWRITTEN, - FLAG_ARCHIVE_CONFLICT_MIXED, - FLAG_PLUGIN_SELECTED, - FLAG_ALTERNATE_GAME - }; - - enum EContent { - CONTENT_PLUGIN, - CONTENT_TEXTURE, - CONTENT_MESH, - CONTENT_BSA, - CONTENT_INTERFACE, - CONTENT_SOUND, - CONTENT_SCRIPT, - CONTENT_SKSE, - CONTENT_SKSEFILES, - CONTENT_SKYPROC, - CONTENT_MCM, - CONTENT_INI, - CONTENT_MODGROUP - }; - - static const int NUM_CONTENT_TYPES = CONTENT_MODGROUP + 1; - - enum EHighlight { - HIGHLIGHT_NONE = 0, - HIGHLIGHT_INVALID = 1, - HIGHLIGHT_CENTER = 2, - HIGHLIGHT_IMPORTANT = 4, - HIGHLIGHT_PLUGIN = 8 - }; - - enum EEndorsedState { - ENDORSED_FALSE, - ENDORSED_TRUE, - ENDORSED_UNKNOWN, - ENDORSED_NEVER - }; - - enum EModType { - MOD_DEFAULT, - MOD_DLC, - MOD_CC - }; - - -public: - - /** - * @brief read the mod directory and Mod ModInfo objects for all subdirectories - **/ - static void updateFromDisc(const QString &modDirectory, - MOShared::DirectoryEntry **directoryStructure, - PluginContainer *pluginContainer, - bool displayForeign, - MOBase::IPluginGame const *game); - - static void clear() { s_Collection.clear(); s_ModsByName.clear(); s_ModsByModID.clear(); } - - /** - * @brief retrieve the number of mods - * - * @return number of mods - **/ - static unsigned int getNumMods(); - - /** - * @brief retrieve a ModInfo object based on its index - * - * @param index the index to look up. the maximum is getNumMods() - 1 - * @return a reference counting pointer to the mod info. - * @note since the pointer is reference counting, the pointer remains valid even if the collection is refreshed in a different thread - **/ - static ModInfo::Ptr getByIndex(unsigned int index); - - /** - * @brief retrieve a ModInfo object based on its nexus mod id - * - * @param modID the nexus mod id to look up - * @return a reference counting pointer to the mod info - * @todo in its current form, this function is broken! There may be multiple mods with the same nexus id, - * this function will return only one of them - **/ - static std::vector getByModID(QString game, int modID); - - /** - * @brief remove a mod by index - * - * this physically deletes the specified mod from the disc and updates the ModInfo collection - * but not other structures that reference mods - * @param index index of the mod to delete - * @return true if removal was successful, fals otherwise - **/ - static bool removeMod(unsigned int index); - - /** - * @brief retrieve the mod index by the mod name - * - * @param name name of the mod to look up - * @return the index of the mod. If the mod doesn't exist, UINT_MAX is returned - **/ - static unsigned int getIndex(const QString &name); - - /** - * @brief find the first mod that fulfills the filter function (after no particular order) - * @param filter a function to filter by. should return true for a match - * @return index of the matching mod or UINT_MAX if there wasn't a match - */ - static unsigned int findMod(const boost::function &filter); - - /** - * @brief check a bunch of mods for updates - * @param modIDs list of mods (Nexus Mod IDs) to check for updates - * @return - */ - static void checkChunkForUpdate(PluginContainer *pluginContainer, const std::vector &modIDs, QObject *receiver, QString gameName); - - /** - * @brief query nexus information for every mod and update the "newest version" information - **/ - static int checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver); - - /** - * @brief create a new mod from the specified directory and add it to the collection - * @param dir directory to create from - * @return pointer to the info-structure of the newly created/added mod - */ - static ModInfo::Ptr createFrom(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &dir, MOShared::DirectoryEntry **directoryStructure); - - /** - * @brief create a new "foreign-managed" mod from a tuple of plugin and archives - * @param espName name of the plugin - * @param bsaNames names of archives - * @return a new mod - */ - static ModInfo::Ptr createFromPlugin(const QString &modName, const QString &espName, const QStringList &bsaNames, ModInfo::EModType modType, MOShared::DirectoryEntry **directoryStructure, PluginContainer *pluginContainer); - - /** - * @brief retieve a name for one of the CONTENT_ enums - * @param contentType the content value - * @return a display string - */ - static QString getContentTypeName(int contentType); - - virtual bool isRegular() const { return false; } - - virtual bool isEmpty() const { return false; } - - /** - * @brief test if there is a newer version of the mod - * - * test if there is a newer version of the mod. This does NOT cause - * information to be retrieved from the nexus, it will only test version information already - * available locally. Use checkAllForUpdate() to update this version information - * - * @return true if there is a newer version - **/ - virtual bool updateAvailable() const = 0; - - /** - * @return true if the update currently available is ignored - */ - virtual bool updateIgnored() const = 0; - - /** - * @brief test if the "newest" version of the mod is older than the installed version - * - * test if there is a newer version of the mod. This does NOT cause - * information to be retrieved from the nexus, it will only test version information already - * available locally. Use checkAllForUpdate() to update this version information - * - * @return true if the newest version is older than the installed one - **/ - virtual bool downgradeAvailable() const = 0; - - /** - * @brief request an update of nexus description for this mod. - * - * This requests mod information from the nexus. This is an asynchronous request, - * so there is no immediate effect of this call. - * Right now, Mod Organizer interprets the "newest version" and "description" from the - * response, though the description is only stored in memory - * - **/ - virtual bool updateNXMInfo() = 0; - - /** - * @brief assign or unassign the specified category - * - * Every mod can have an arbitrary number of categories assigned to it - * - * @param categoryID id of the category to set - * @param active determines wheter the category is assigned or unassigned - * @note this function does not test whether categoryID actually identifies a valid category - **/ - virtual void setCategory(int categoryID, bool active) = 0; - - /** - * @brief changes the comments (manually set information displayed in the mod list) for this mod - * @param comments new comments - */ - virtual void setComments(const QString &comments) = 0; - - /** - * @brief change the notes (manually set information) for this mod - * @param notes new notes - */ - virtual void setNotes(const QString ¬es) = 0; - - /** - * @brief set/change the source game of this mod - * - * @param gameName the source game shortName - */ - virtual void setGameName(QString gameName) = 0; - - /** - * @brief set/change the nexus mod id of this mod - * - * @param modID the nexus mod id - **/ - virtual void setNexusID(int modID) = 0; - - /** - * @brief set/change the version of this mod - * @param version new version of the mod - */ - virtual void setVersion(const MOBase::VersionInfo &version); - - /** - * @brief Controls if mod should be highlighted based on plugin selection - * @param isSelected whether or not the plugin has a selected mod - **/ - virtual void setPluginSelected(const bool &isSelected); - - /** - * @brief set the newest version of this mod on the nexus - * - * this can be used to overwrite the version of a mod without actually - * updating the mod - * - * @param version the new version to use - * @todo this function should be made obsolete. All queries for mod information should go through - * this class so no public function for this change is required - **/ - virtual void setNewestVersion(const MOBase::VersionInfo &version) = 0; - - /** - * @brief sets the repository that was used to download the mod - */ - virtual void setRepository(const QString &) {} - - /** - * @brief changes/updates the nexus description text - * @param description the current description text - */ - virtual void setNexusDescription(const QString &description) = 0; - - /** - * @brief sets the file this mod was installed from - * @param fileName name of the file - */ - virtual void setInstallationFile(const QString &fileName) = 0; - - /** - * @brief sets the category id from a nexus category id. Conversion to MO id happens internally - * @param categoryID the nexus category id - * @note if a mapping is not possible, the category is set to the default value - */ - virtual void addNexusCategory(int categoryID) = 0; - - virtual void addCategory(const QString &categoryName) override; - virtual bool removeCategory(const QString &categoryName) override; - virtual QStringList categories() override; - - /** - * update the endorsement state for the mod. This only changes the - * buffered state, it does not sync with Nexus - * @param endorsed the new endorsement state - */ - virtual void setIsEndorsed(bool endorsed) = 0; - - /** - * set the mod to "i don't intend to endorse". The mod will not show as unendorsed but can still be endorsed - */ - virtual void setNeverEndorse() = 0; - - /** - * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices - * @return true if the mod was successfully removed - **/ - virtual bool remove() = 0; - - /** - * @brief endorse or un-endorse the mod. This will sync with nexus! - * @param doEndorse if true, the mod is endorsed, if false, it's un-endorsed. - * @note if doEndorse doesn't differ from the current value, nothing happens. - */ - virtual void endorse(bool doEndorse) = 0; - - /** - * @brief clear all caches held for this mod - */ - virtual void clearCaches() {} - - /** - * @brief getter for the mod name - * - * @return the mod name - **/ - virtual QString name() const = 0; - - /** - * @brief getter for an internal name. This is usually the same as the regular name, but with special mod types it might be - * this is used to distinguish between mods that have the same visible name - * @return internal mod name - */ - virtual QString internalName() const { return name(); } - - /** - * @brief getter for the mod path - * - * @return the (absolute) path to the mod - **/ - virtual QString absolutePath() const = 0; - - /** - * @brief getter for the installation file - * - * @return file used to install this mod from - */ - virtual QString getInstallationFile() const = 0; - - /** - * @return version object for machine based comparisons - **/ - virtual MOBase::VersionInfo getVersion() const { return m_Version; } - - /** - * @brief getter for the newest version number of this mod - * - * @return newest version of the mod - **/ - virtual MOBase::VersionInfo getNewestVersion() const = 0; - - /** - * @return the repository from which the file was downloaded. Only relevant regular mods - */ - virtual QString repository() const { return ""; } - - /** - * @brief ignore the newest version for updates - */ - virtual void ignoreUpdate(bool ignore) = 0; - - /** - * @brief getter for the nexus mod id - * - * @return the nexus mod id. may be 0 if the mod id isn't known or doesn't exist - **/ - virtual int getNexusID() const = 0; - - /** - * @brief getter for the source game repository - * - * @return the source game repository. should default to the active game. - **/ - virtual QString getGameName() const = 0; - - /** - * @return the fixed priority of mods of this type or INT_MIN if the priority of mods - * needs to be user-modifiable. Can be < 0 to force a priority below user-modifable mods - * or INT_MAX to force priority above all user-modifiables - */ - virtual int getFixedPriority() const = 0; - - /** - * @return true if the mod is always enabled - */ - virtual bool alwaysEnabled() const { return false; } - - /** - * @return true if the mod can be updated - */ - virtual bool canBeUpdated() const { return false; } - - /** - * @return true if the mod can be enabled/disabled - */ - virtual bool canBeEnabled() const { return false; } - - /** - * @return a list of flags for this mod - */ - virtual std::vector getFlags() const = 0; - - /** - * @return a list of content types contained in a mod - */ - virtual std::vector getContents() const { return std::vector(); } - - /** - * @brief test if the specified flag is set for this mod - * @param flag the flag to test - * @return true if the flag is set, false otherwise - */ - bool hasFlag(EFlag flag) const; - - /** - * @brief test if the mods contains the specified content - * @param content the content to test - * @return true if the content is there, false otherwise - */ - bool hasContent(ModInfo::EContent content) const; - - /** - * @return an indicator if and how this mod should be highlighted by the UI - */ - virtual int getHighlight() const { return HIGHLIGHT_NONE; } - - /** - * @return list of names of ini tweaks - **/ - virtual std::vector getIniTweaks() const = 0; - - /** - * @return a description about the mod, to be displayed in the ui - */ - virtual QString getDescription() const = 0; - - /** - * @return comments for this mod - */ - virtual QString comments() const = 0; - - /** - * @return notes for this mod - */ - virtual QString notes() const = 0; - - /** - * @return creation time of this mod - */ - virtual QDateTime creationTime() const = 0; - - /** - * @return nexus description of the mod (html) - */ - virtual QString getNexusDescription() const = 0; - - /** - * @return last time nexus was queried for infos on this mod - */ - virtual QDateTime getLastNexusQuery() const = 0; - - /** - * @return a list of files that, if they exist in the data directory are treated as files in THIS mod - */ - virtual QStringList stealFiles() const { return QStringList(); } - - /** - * @return a list of archives belonging to this mod (as absolute file paths) - */ - virtual QStringList archives(bool checkOnDisk = false) = 0; - - /* - *@return the color choosen by the user for the mod/separator - */ - virtual QColor getColor() { return QColor(); } - - /* - *@return true if the color has been set successfully. - */ - virtual void setColor(QColor color) { } - - /** - * @brief adds the information that a file has been installed into this mod - * @param modId id of the mod installed - * @param fileId id of the file installed - */ - virtual void addInstalledFile(int modId, int fileId) = 0; - - /** - * @brief test if the mod belongs to the specified category - * - * @param categoryID the category to test for. - * @return true if the mod belongs to the specified category - * @note this does not verify the id actually identifies a category - **/ - bool categorySet(int categoryID) const; - - /** - * @brief retrive the whole list of categories (as ids) this mod belongs to - * - * @return list of categories - **/ - const std::set &getCategories() const { return m_Categories; } - - /** - * @return id of the primary category of this mod - */ - int getPrimaryCategory() const { return m_PrimaryCategory; } - - /** - * @brief sets the new primary category of the mod - * @param categoryID the category to set - */ - virtual void setPrimaryCategory(int categoryID) { m_PrimaryCategory = categoryID; } - - /** - * @return true if this mod is considered "valid", that is: it contains data used by the game - **/ - virtual bool isValid() const { return m_Valid; } - - /** - * @return true if the file has been endorsed on nexus - */ - virtual EEndorsedState endorsedState() const { return ENDORSED_NEVER; } - - /** - * @brief updates the valid-flag for this mod - */ - void testValid(); - - /** - * @brief updates the mod to flag it as converted in order to ignore the alternate game warning - */ - virtual void markConverted(bool converted) {} - - /** - * @brief updates the mod to flag it as valid in order to ignore the invalid game data flag - */ - virtual void markValidated(bool validated) {} - - /** - * @brief reads meta information from disk - */ - virtual void readMeta() {} - - /** - * @brief stores meta information back to disk - */ - virtual void saveMeta() {} - - /** - * @return retrieve list of mods (as mod index) that are overwritten by this one. Updates may be delayed - */ - virtual std::set getModOverwrite() { return std::set(); } - - /** - * @return list of mods (as mod index) that overwrite this one. Updates may be delayed - */ - virtual std::set getModOverwritten() { return std::set(); } - - /** - * @return retrieve list of mods (as mod index) with archives that are overwritten by this one. Updates may be delayed - */ - virtual std::set getModArchiveOverwrite() { return std::set(); } - - /** - * @return list of mods (as mod index) with archives that overwrite this one. Updates may be delayed - */ - virtual std::set getModArchiveOverwritten() { return std::set(); } - - /** - * @return retrieve list of mods (as mod index) with archives that are overwritten by thos mod's loose files. Updates may be delayed - */ - virtual std::set getModArchiveLooseOverwrite() { return std::set(); } - - /** - * @return list of mods (as mod index) with loose files that overwrite this one's archive files. Updates may be delayed - */ - virtual std::set getModArchiveLooseOverwritten() { return std::set(); } - - /** - * @brief update conflict information - */ - virtual void doConflictCheck() const {} - - /** - * @brief set the URL for a mod - */ - virtual void setURL(QString const &) {} - - /** - * @returns the URL for a mod - */ - virtual QString getURL() const { return ""; } - -signals: - - /** - * @brief emitted whenever the information of a mod changes - * - * @param success true if the mod details were updated successfully, false if not - **/ - void modDetailsUpdated(bool success); - -protected: - - ModInfo(PluginContainer *pluginContainer); - - static void updateIndices(); - static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS); - -private: - - static void createFromOverwrite(PluginContainer *pluginContainer); - -protected: - - static std::vector s_Collection; - static std::map s_ModsByName; - - int m_PrimaryCategory; - std::set m_Categories; - - MOBase::VersionInfo m_Version; - - bool m_PluginSelected = false; - -private: - - static QMutex s_Mutex; - static std::map, std::vector > s_ModsByModID; - static int s_NextID; - - bool m_Valid; - -}; - - -#endif // MODINFO_H +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef MODINFO_H +#define MODINFO_H + +#include "imodinterface.h" +#include "versioninfo.h" + +class PluginContainer; + +class QDateTime; +class QDir; +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace MOBase { class IPluginGame; } +namespace MOShared { class DirectoryEntry; } + +/** + * @brief Represents meta information about a single mod. + * + * Represents meta information about a single mod. The class interface is used + * to manage the mod collection + * + **/ +class ModInfo : public QObject, public MOBase::IModInterface +{ + + Q_OBJECT + +public: + + typedef QSharedPointer Ptr; + + static QString s_HiddenExt; + + enum EFlag { + FLAG_INVALID, + FLAG_BACKUP, + FLAG_SEPARATOR, + FLAG_OVERWRITE, + FLAG_FOREIGN, + FLAG_NOTENDORSED, + FLAG_NOTES, + FLAG_CONFLICT_OVERWRITE, + FLAG_CONFLICT_OVERWRITTEN, + FLAG_CONFLICT_MIXED, + FLAG_CONFLICT_REDUNDANT, + FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE, + FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN, + FLAG_ARCHIVE_CONFLICT_OVERWRITE, + FLAG_ARCHIVE_CONFLICT_OVERWRITTEN, + FLAG_ARCHIVE_CONFLICT_MIXED, + FLAG_PLUGIN_SELECTED, + FLAG_ALTERNATE_GAME + }; + + enum EContent { + CONTENT_PLUGIN, + CONTENT_TEXTURE, + CONTENT_MESH, + CONTENT_BSA, + CONTENT_INTERFACE, + CONTENT_SOUND, + CONTENT_SCRIPT, + CONTENT_SKSE, + CONTENT_SKSEFILES, + CONTENT_SKYPROC, + CONTENT_MCM, + CONTENT_INI, + CONTENT_MODGROUP + }; + + static const int NUM_CONTENT_TYPES = CONTENT_MODGROUP + 1; + + enum EHighlight { + HIGHLIGHT_NONE = 0, + HIGHLIGHT_INVALID = 1, + HIGHLIGHT_CENTER = 2, + HIGHLIGHT_IMPORTANT = 4, + HIGHLIGHT_PLUGIN = 8 + }; + + enum EEndorsedState { + ENDORSED_FALSE, + ENDORSED_TRUE, + ENDORSED_UNKNOWN, + ENDORSED_NEVER + }; + + enum EModType { + MOD_DEFAULT, + MOD_DLC, + MOD_CC + }; + + +public: + + /** + * @brief read the mod directory and Mod ModInfo objects for all subdirectories + **/ + static void updateFromDisc(const QString &modDirectory, + MOShared::DirectoryEntry **directoryStructure, + PluginContainer *pluginContainer, + bool displayForeign, + MOBase::IPluginGame const *game); + + static void clear() { s_Collection.clear(); s_ModsByName.clear(); s_ModsByModID.clear(); } + + /** + * @brief retrieve the number of mods + * + * @return number of mods + **/ + static unsigned int getNumMods(); + + /** + * @brief retrieve a ModInfo object based on its index + * + * @param index the index to look up. the maximum is getNumMods() - 1 + * @return a reference counting pointer to the mod info. + * @note since the pointer is reference counting, the pointer remains valid even if the collection is refreshed in a different thread + **/ + static ModInfo::Ptr getByIndex(unsigned int index); + + /** + * @brief retrieve a ModInfo object based on its nexus mod id + * + * @param modID the nexus mod id to look up + * @return a reference counting pointer to the mod info + * @todo in its current form, this function is broken! There may be multiple mods with the same nexus id, + * this function will return only one of them + **/ + static std::vector getByModID(QString game, int modID); + + /** + * @brief remove a mod by index + * + * this physically deletes the specified mod from the disc and updates the ModInfo collection + * but not other structures that reference mods + * @param index index of the mod to delete + * @return true if removal was successful, fals otherwise + **/ + static bool removeMod(unsigned int index); + + /** + * @brief retrieve the mod index by the mod name + * + * @param name name of the mod to look up + * @return the index of the mod. If the mod doesn't exist, UINT_MAX is returned + **/ + static unsigned int getIndex(const QString &name); + + /** + * @brief find the first mod that fulfills the filter function (after no particular order) + * @param filter a function to filter by. should return true for a match + * @return index of the matching mod or UINT_MAX if there wasn't a match + */ + static unsigned int findMod(const boost::function &filter); + + /** + * @brief run a limited batch of mod update checks for "newest version" information + */ + static int autoUpdateCheck(PluginContainer *pluginContainer, QObject *receiver); + + /** + * @brief query nexus information for every mod and update the "newest version" information + **/ + static int checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver); + + /** + * @brief create a new mod from the specified directory and add it to the collection + * @param dir directory to create from + * @return pointer to the info-structure of the newly created/added mod + */ + static ModInfo::Ptr createFrom(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &dir, MOShared::DirectoryEntry **directoryStructure); + + /** + * @brief create a new "foreign-managed" mod from a tuple of plugin and archives + * @param espName name of the plugin + * @param bsaNames names of archives + * @return a new mod + */ + static ModInfo::Ptr createFromPlugin(const QString &modName, const QString &espName, const QStringList &bsaNames, ModInfo::EModType modType, MOShared::DirectoryEntry **directoryStructure, PluginContainer *pluginContainer); + + /** + * @brief retieve a name for one of the CONTENT_ enums + * @param contentType the content value + * @return a display string + */ + static QString getContentTypeName(int contentType); + + virtual bool isRegular() const { return false; } + + virtual bool isEmpty() const { return false; } + + /** + * @brief test if there is a newer version of the mod + * + * test if there is a newer version of the mod. This does NOT cause + * information to be retrieved from the nexus, it will only test version information already + * available locally. Use checkAllForUpdate() to update this version information + * + * @return true if there is a newer version + **/ + virtual bool updateAvailable() const = 0; + + /** + * @return true if the update currently available is ignored + */ + virtual bool updateIgnored() const = 0; + + /** + * @brief test if the "newest" version of the mod is older than the installed version + * + * test if there is a newer version of the mod. This does NOT cause + * information to be retrieved from the nexus, it will only test version information already + * available locally. Use checkAllForUpdate() to update this version information + * + * @return true if the newest version is older than the installed one + **/ + virtual bool downgradeAvailable() const = 0; + + /** + * @brief request an update of nexus description for this mod. + * + * This requests mod information from the nexus. This is an asynchronous request, + * so there is no immediate effect of this call. + * Right now, Mod Organizer interprets the "newest version" and "description" from the + * response, though the description is only stored in memory + * + **/ + virtual bool updateNXMInfo() = 0; + + /** + * @brief assign or unassign the specified category + * + * Every mod can have an arbitrary number of categories assigned to it + * + * @param categoryID id of the category to set + * @param active determines wheter the category is assigned or unassigned + * @note this function does not test whether categoryID actually identifies a valid category + **/ + virtual void setCategory(int categoryID, bool active) = 0; + + /** + * @brief changes the comments (manually set information displayed in the mod list) for this mod + * @param comments new comments + */ + virtual void setComments(const QString &comments) = 0; + + /** + * @brief change the notes (manually set information) for this mod + * @param notes new notes + */ + virtual void setNotes(const QString ¬es) = 0; + + /** + * @brief set/change the source game of this mod + * + * @param gameName the source game shortName + */ + virtual void setGameName(QString gameName) = 0; + + /** + * @brief set/change the nexus mod id of this mod + * + * @param modID the nexus mod id + **/ + virtual void setNexusID(int modID) = 0; + + /** + * @brief set/change the version of this mod + * @param version new version of the mod + */ + virtual void setVersion(const MOBase::VersionInfo &version); + + /** + * @brief Controls if mod should be highlighted based on plugin selection + * @param isSelected whether or not the plugin has a selected mod + **/ + virtual void setPluginSelected(const bool &isSelected); + + /** + * @brief set the newest version of this mod on the nexus + * + * this can be used to overwrite the version of a mod without actually + * updating the mod + * + * @param version the new version to use + * @todo this function should be made obsolete. All queries for mod information should go through + * this class so no public function for this change is required + **/ + virtual void setNewestVersion(const MOBase::VersionInfo &version) = 0; + + /** + * @brief sets the repository that was used to download the mod + */ + virtual void setRepository(const QString &) {} + + /** + * @brief changes/updates the nexus description text + * @param description the current description text + */ + virtual void setNexusDescription(const QString &description) = 0; + + /** + * @brief sets the file this mod was installed from + * @param fileName name of the file + */ + virtual void setInstallationFile(const QString &fileName) = 0; + + /** + * @brief sets the category id from a nexus category id. Conversion to MO id happens internally + * @param categoryID the nexus category id + * @note if a mapping is not possible, the category is set to the default value + */ + virtual void addNexusCategory(int categoryID) = 0; + + virtual void addCategory(const QString &categoryName) override; + virtual bool removeCategory(const QString &categoryName) override; + virtual QStringList categories() override; + + /** + * update the endorsement state for the mod. This only changes the + * buffered state, it does not sync with Nexus + * @param endorsed the new endorsement state + */ + virtual void setIsEndorsed(bool endorsed) = 0; + + /** + * set the mod to "i don't intend to endorse". The mod will not show as unendorsed but can still be endorsed + */ + virtual void setNeverEndorse() = 0; + + /** + * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices + * @return true if the mod was successfully removed + **/ + virtual bool remove() = 0; + + /** + * @brief endorse or un-endorse the mod. This will sync with nexus! + * @param doEndorse if true, the mod is endorsed, if false, it's un-endorsed. + * @note if doEndorse doesn't differ from the current value, nothing happens. + */ + virtual void endorse(bool doEndorse) = 0; + + /** + * @brief clear all caches held for this mod + */ + virtual void clearCaches() {} + + /** + * @brief getter for the mod name + * + * @return the mod name + **/ + virtual QString name() const = 0; + + /** + * @brief getter for an internal name. This is usually the same as the regular name, but with special mod types it might be + * this is used to distinguish between mods that have the same visible name + * @return internal mod name + */ + virtual QString internalName() const { return name(); } + + /** + * @brief getter for the mod path + * + * @return the (absolute) path to the mod + **/ + virtual QString absolutePath() const = 0; + + /** + * @brief getter for the installation file + * + * @return file used to install this mod from + */ + virtual QString getInstallationFile() const = 0; + + /** + * @return version object for machine based comparisons + **/ + virtual MOBase::VersionInfo getVersion() const { return m_Version; } + + /** + * @brief getter for the newest version number of this mod + * + * @return newest version of the mod + **/ + virtual MOBase::VersionInfo getNewestVersion() const = 0; + + /** + * @return the repository from which the file was downloaded. Only relevant regular mods + */ + virtual QString repository() const { return ""; } + + /** + * @brief ignore the newest version for updates + */ + virtual void ignoreUpdate(bool ignore) = 0; + + /** + * @brief getter for the nexus mod id + * + * @return the nexus mod id. may be 0 if the mod id isn't known or doesn't exist + **/ + virtual int getNexusID() const = 0; + + /** + * @brief getter for the source game repository + * + * @return the source game repository. should default to the active game. + **/ + virtual QString getGameName() const = 0; + + /** + * @return the fixed priority of mods of this type or INT_MIN if the priority of mods + * needs to be user-modifiable. Can be < 0 to force a priority below user-modifable mods + * or INT_MAX to force priority above all user-modifiables + */ + virtual int getFixedPriority() const = 0; + + /** + * @return true if the mod is always enabled + */ + virtual bool alwaysEnabled() const { return false; } + + /** + * @return true if the mod can be updated + */ + virtual bool canBeUpdated() const { return false; } + + /** + * @return true if the mod can be enabled/disabled + */ + virtual bool canBeEnabled() const { return false; } + + /** + * @return a list of flags for this mod + */ + virtual std::vector getFlags() const = 0; + + /** + * @return a list of content types contained in a mod + */ + virtual std::vector getContents() const { return std::vector(); } + + /** + * @brief test if the specified flag is set for this mod + * @param flag the flag to test + * @return true if the flag is set, false otherwise + */ + bool hasFlag(EFlag flag) const; + + /** + * @brief test if the mods contains the specified content + * @param content the content to test + * @return true if the content is there, false otherwise + */ + bool hasContent(ModInfo::EContent content) const; + + /** + * @return an indicator if and how this mod should be highlighted by the UI + */ + virtual int getHighlight() const { return HIGHLIGHT_NONE; } + + /** + * @return list of names of ini tweaks + **/ + virtual std::vector getIniTweaks() const = 0; + + /** + * @return a description about the mod, to be displayed in the ui + */ + virtual QString getDescription() const = 0; + + /** + * @return comments for this mod + */ + virtual QString comments() const = 0; + + /** + * @return notes for this mod + */ + virtual QString notes() const = 0; + + /** + * @return creation time of this mod + */ + virtual QDateTime creationTime() const = 0; + + /** + * @return nexus description of the mod (html) + */ + virtual QString getNexusDescription() const = 0; + + /** + * @brief get the last time nexus was checked for file updates on this mod + */ + virtual QDateTime getLastNexusUpdate() const = 0; + + /** + * @brief set the last time nexus was checked for file updates on this mod + */ + virtual void setLastNexusUpdate(QDateTime time) = 0; + + /** + * @return last time nexus was queried for infos on this mod + */ + virtual QDateTime getLastNexusQuery() const = 0; + + /** + * @brief set the last time nexus was queried for info on this mod + */ + virtual void setLastNexusQuery(QDateTime time) = 0; + + /** + * @return a list of files that, if they exist in the data directory are treated as files in THIS mod + */ + virtual QStringList stealFiles() const { return QStringList(); } + + /** + * @return a list of archives belonging to this mod (as absolute file paths) + */ + virtual QStringList archives(bool checkOnDisk = false) = 0; + + /* + *@return the color choosen by the user for the mod/separator + */ + virtual QColor getColor() { return QColor(); } + + /* + *@return true if the color has been set successfully. + */ + virtual void setColor(QColor color) { } + + /** + * @brief adds the information that a file has been installed into this mod + * @param modId id of the mod installed + * @param fileId id of the file installed + */ + virtual void addInstalledFile(int modId, int fileId) = 0; + + /** + * @brief test if the mod belongs to the specified category + * + * @param categoryID the category to test for. + * @return true if the mod belongs to the specified category + * @note this does not verify the id actually identifies a category + **/ + bool categorySet(int categoryID) const; + + /** + * @brief retrive the whole list of categories (as ids) this mod belongs to + * + * @return list of categories + **/ + const std::set &getCategories() const { return m_Categories; } + + /** + * @return id of the primary category of this mod + */ + int getPrimaryCategory() const { return m_PrimaryCategory; } + + /** + * @brief sets the new primary category of the mod + * @param categoryID the category to set + */ + virtual void setPrimaryCategory(int categoryID) { m_PrimaryCategory = categoryID; } + + /** + * @return true if this mod is considered "valid", that is: it contains data used by the game + **/ + virtual bool isValid() const { return m_Valid; } + + /** + * @return true if the file has been endorsed on nexus + */ + virtual EEndorsedState endorsedState() const { return ENDORSED_NEVER; } + + /** + * @brief updates the valid-flag for this mod + */ + void testValid(); + + /** + * @brief updates the mod to flag it as converted in order to ignore the alternate game warning + */ + virtual void markConverted(bool converted) {} + + /** + * @brief updates the mod to flag it as valid in order to ignore the invalid game data flag + */ + virtual void markValidated(bool validated) {} + + /** + * @brief reads meta information from disk + */ + virtual void readMeta() {} + + /** + * @brief stores meta information back to disk + */ + virtual void saveMeta() {} + + /** + * @return retrieve list of mods (as mod index) that are overwritten by this one. Updates may be delayed + */ + virtual std::set getModOverwrite() { return std::set(); } + + /** + * @return list of mods (as mod index) that overwrite this one. Updates may be delayed + */ + virtual std::set getModOverwritten() { return std::set(); } + + /** + * @return retrieve list of mods (as mod index) with archives that are overwritten by this one. Updates may be delayed + */ + virtual std::set getModArchiveOverwrite() { return std::set(); } + + /** + * @return list of mods (as mod index) with archives that overwrite this one. Updates may be delayed + */ + virtual std::set getModArchiveOverwritten() { return std::set(); } + + /** + * @return retrieve list of mods (as mod index) with archives that are overwritten by thos mod's loose files. Updates may be delayed + */ + virtual std::set getModArchiveLooseOverwrite() { return std::set(); } + + /** + * @return list of mods (as mod index) with loose files that overwrite this one's archive files. Updates may be delayed + */ + virtual std::set getModArchiveLooseOverwritten() { return std::set(); } + + /** + * @brief update conflict information + */ + virtual void doConflictCheck() const {} + + /** + * @brief set the URL for a mod + */ + virtual void setURL(QString const &) {} + + /** + * @returns the URL for a mod + */ + virtual QString getURL() const { return ""; } + +signals: + + /** + * @brief emitted whenever the information of a mod changes + * + * @param success true if the mod details were updated successfully, false if not + **/ + void modDetailsUpdated(bool success); + +protected: + + ModInfo(PluginContainer *pluginContainer); + + static void updateIndices(); + static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS); + +private: + + static void createFromOverwrite(PluginContainer *pluginContainer); + +protected: + + static std::vector s_Collection; + static std::map s_ModsByName; + + int m_PrimaryCategory; + std::set m_Categories; + + MOBase::VersionInfo m_Version; + + bool m_PluginSelected = false; + +private: + + static QMutex s_Mutex; + static std::map, std::vector > s_ModsByModID; + static int s_NextID; + + bool m_Valid; + +}; + + +#endif // MODINFO_H diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index d086de08..db3e5de1 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -929,7 +929,7 @@ void ModInfoDialog::activateNexusTab() visitNexusLabel->setToolTip(nexusLink); if (m_ModInfo->getNexusDescription().isEmpty() || - QDateTime::currentDateTime() > m_ModInfo->getLastNexusQuery().addDays(1)) { + QDateTime::currentDateTimeUtc() > m_ModInfo->getLastNexusQuery().addDays(1)) { refreshNexusData(modID); } else { this->modDetailsUpdated(true); diff --git a/src/modinfoforeign.h b/src/modinfoforeign.h index 20bfab2a..0702f268 100644 --- a/src/modinfoforeign.h +++ b/src/modinfoforeign.h @@ -47,7 +47,10 @@ public: virtual std::vector getFlags() const; virtual int getHighlight() const; virtual QString getDescription() const; + virtual QDateTime getLastNexusUpdate() const { return QDateTime(); } + virtual void setLastNexusUpdate(QDateTime time) {} virtual QDateTime getLastNexusQuery() const { return QDateTime(); } + virtual void setLastNexusQuery(QDateTime time) {} virtual QString getNexusDescription() const { return QString(); } virtual int getFixedPriority() const { return INT_MIN; } virtual QStringList archives(bool checkOnDisk = false) { return m_Archives; } diff --git a/src/modinfooverwrite.h b/src/modinfooverwrite.h index b68fb15b..d2882301 100644 --- a/src/modinfooverwrite.h +++ b/src/modinfooverwrite.h @@ -50,7 +50,10 @@ public: virtual std::vector getFlags() const; virtual int getHighlight() const; virtual QString getDescription() const; + virtual QDateTime getLastNexusUpdate() const { return QDateTime(); } + virtual void setLastNexusUpdate(QDateTime time) {} virtual QDateTime getLastNexusQuery() const { return QDateTime(); } + virtual void setLastNexusQuery(QDateTime time) {} virtual QString getNexusDescription() const { return QString(); } virtual QStringList archives(bool checkOnDisk = false); virtual void addInstalledFile(int, int) {} diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 834c09ae..57c4baad 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -98,6 +98,7 @@ void ModInfoRegular::readMeta() m_Validated = metaFile.value("validated", false).toBool(); m_URL = metaFile.value("url", "").toString(); m_LastNexusQuery = QDateTime::fromString(metaFile.value("lastNexusQuery", "").toString(), Qt::ISODate); + m_LastNexusUpdate = QDateTime::fromString(metaFile.value("lastNexusUpdate", "").toString(), Qt::ISODate); m_Color = metaFile.value("color",QColor()).value(); if (metaFile.contains("endorsed")) { if (metaFile.value("endorsed").canConvert()) { @@ -161,6 +162,7 @@ void ModInfoRegular::saveMeta() metaFile.setValue("nexusDescription", m_NexusDescription); metaFile.setValue("url", m_URL); metaFile.setValue("lastNexusQuery", m_LastNexusQuery.toString(Qt::ISODate)); + metaFile.setValue("lastNexusUpdate", m_LastNexusUpdate.toString(Qt::ISODate)); metaFile.setValue("converted", m_Converted); metaFile.setValue("validated", m_Validated); metaFile.setValue("color", m_Color); @@ -225,8 +227,8 @@ void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant re else setEndorsedState(ENDORSED_FALSE); } - m_LastNexusQuery = QDateTime::currentDateTime(); - //m_MetaInfoChanged = true; + m_LastNexusQuery = QDateTime::currentDateTimeUtc(); + m_MetaInfoChanged = true; saveMeta(); emit modDetailsUpdated(true); } @@ -263,10 +265,14 @@ void ModInfoRegular::nxmRequestFailed(QString, int, int, QVariant userData, QNet bool ModInfoRegular::updateNXMInfo() { - if (m_NexusID > 0) { + QDateTime time = QDateTime::currentDateTimeUtc(); + QDateTime target = m_LastNexusQuery.addSecs(3600); + if (m_NexusID > 0 && time >= target) { m_NexusBridge.requestDescription(m_GameName, m_NexusID, QVariant()); return true; } + QString warning("Please wait until %1 to request updated mod info from Nexus!"); + qWarning() << warning.arg(target.toLocalTime().time().toString(Qt::DefaultLocaleShortDate)); return false; } @@ -483,6 +489,15 @@ void ModInfoRegular::ignoreUpdate(bool ignore) m_MetaInfoChanged = true; } +bool ModInfoRegular::canBeUpdated() const +{ + QDateTime now = QDateTime::currentDateTimeUtc(); + QDateTime target = m_LastNexusUpdate.addSecs(3600); + if (now >= target) + return m_NexusID > 0; + return false; +} + std::vector ModInfoRegular::getFlags() const { @@ -628,11 +643,32 @@ ModInfoRegular::EEndorsedState ModInfoRegular::endorsedState() const return m_EndorsedState; } +QDateTime ModInfoRegular::getLastNexusUpdate() const +{ + return m_LastNexusUpdate; +} + +void ModInfoRegular::setLastNexusUpdate(QDateTime time) +{ + m_LastNexusUpdate = time; + m_MetaInfoChanged = true; + saveMeta(); + emit modDetailsUpdated(true); +} + QDateTime ModInfoRegular::getLastNexusQuery() const { return m_LastNexusQuery; } +void ModInfoRegular::setLastNexusQuery(QDateTime time) +{ + m_LastNexusQuery = time; + m_MetaInfoChanged = true; + saveMeta(); + emit modDetailsUpdated(true); +} + void ModInfoRegular::setURL(QString const &url) { m_URL = url; diff --git a/src/modinforegular.h b/src/modinforegular.h index fdb0e672..8e3952e8 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -256,7 +256,7 @@ public: /** * @return true if the mod can be updated */ - virtual bool canBeUpdated() const { return m_NexusID > 0; } + virtual bool canBeUpdated() const; /** * @return true if the mod can be enabled/disabled @@ -315,11 +315,26 @@ public: */ virtual EEndorsedState endorsedState() const; + /** + * @brief get the last time nexus was checked for file updates on this mod + */ + virtual QDateTime getLastNexusUpdate() const; + + /** + * @brief set the last time nexus was checked for file updates on this mod + */ + virtual void setLastNexusUpdate(QDateTime time); + /** * @return last time nexus was queried for infos on this mod */ virtual QDateTime getLastNexusQuery() const; + /** + * @brief set the last time nexus was queried for info on this mod + */ + virtual void setLastNexusQuery(QDateTime time); + virtual QStringList archives(bool checkOnDisk = false); virtual void setColor(QColor color); @@ -376,6 +391,7 @@ private: QDateTime m_CreationTime; QDateTime m_LastNexusQuery; + QDateTime m_LastNexusUpdate; QColor m_Color; diff --git a/src/modinfoseparator.h b/src/modinfoseparator.h index c5e0f0e5..1f3be861 100644 --- a/src/modinfoseparator.h +++ b/src/modinfoseparator.h @@ -43,8 +43,10 @@ public: virtual QString getInstallationFile() const { return ""; } virtual QString getURL() const { return ""; } virtual QString repository() const { return ""; } - + virtual QDateTime getLastNexusUpdate() const { return QDateTime(); } + virtual void setLastNexusUpdate(QDateTime time) {} virtual QDateTime getLastNexusQuery() const { return QDateTime(); } + virtual void setLastNexusQuery(QDateTime time) {} virtual QDateTime creationTime() const { return QDateTime(); } virtual void getNexusFiles diff --git a/src/modlist.cpp b/src/modlist.cpp index daa6ce73..1ec5b556 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -1,1398 +1,1410 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "modlist.h" - -#include "messagedialog.h" -#include "installationtester.h" -#include "qtgroupingproxy.h" -#include "viewmarkingscrollbar.h" -#include "modlistsortproxy.h" -#include "pluginlist.h" -#include "settings.h" -#include "modinforegular.h" -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - - -using namespace MOBase; - - -ModList::ModList(PluginContainer *pluginContainer, QObject *parent) - : QAbstractItemModel(parent) - , m_Profile(nullptr) - , m_NexusInterface(nullptr) - , m_Modified(false) - , m_FontMetrics(QFont()) - , m_DropOnItems(false) - , m_PluginContainer(pluginContainer) -{ - m_ContentIcons[ModInfo::CONTENT_PLUGIN] = std::make_tuple(":/MO/gui/content/plugin", tr("Game Plugins (ESP/ESM/ESL)")); - m_ContentIcons[ModInfo::CONTENT_INTERFACE] = std::make_tuple(":/MO/gui/content/interface", tr("Interface")); - m_ContentIcons[ModInfo::CONTENT_MESH] = std::make_tuple(":/MO/gui/content/mesh", tr("Meshes")); - m_ContentIcons[ModInfo::CONTENT_BSA] = std::make_tuple(":/MO/gui/content/bsa", tr("Bethesda Archive")); - m_ContentIcons[ModInfo::CONTENT_SCRIPT] = std::make_tuple(":/MO/gui/content/script", tr("Scripts (Papyrus)")); - m_ContentIcons[ModInfo::CONTENT_SKSE] = std::make_tuple(":/MO/gui/content/skse", tr("Script Extender Plugin")); - m_ContentIcons[ModInfo::CONTENT_SKYPROC] = std::make_tuple(":/MO/gui/content/skyproc", tr("SkyProc Patcher")); - m_ContentIcons[ModInfo::CONTENT_SOUND] = std::make_tuple(":/MO/gui/content/sound", tr("Sound or Music")); - m_ContentIcons[ModInfo::CONTENT_TEXTURE] = std::make_tuple(":/MO/gui/content/texture", tr("Textures")); - m_ContentIcons[ModInfo::CONTENT_MCM] = std::make_tuple(":/MO/gui/content/menu", tr("MCM Configuration")); - m_ContentIcons[ModInfo::CONTENT_INI] = std::make_tuple(":/MO/gui/content/inifile", tr("INI files")); - m_ContentIcons[ModInfo::CONTENT_MODGROUP] = std::make_tuple(":/MO/gui/content/modgroup", tr("ModGroup files")); - - m_LastCheck.start(); -} - -ModList::~ModList() -{ - m_ModStateChanged.disconnect_all_slots(); - m_ModMoved.disconnect_all_slots(); -} - -void ModList::setProfile(Profile *profile) -{ - m_Profile = profile; -} - -int ModList::rowCount(const QModelIndex &parent) const -{ - if (!parent.isValid()) { - return ModInfo::getNumMods(); - } else { - return 0; - } -} - -bool ModList::hasChildren(const QModelIndex &parent) const -{ - if (!parent.isValid()) { - return ModInfo::getNumMods() > 0; - } else { - return false; - } -} - - -int ModList::columnCount(const QModelIndex &) const -{ - return COL_LASTCOLUMN + 1; -} - - -QVariant ModList::getOverwriteData(int column, int role) const -{ - switch (role) { - case Qt::DisplayRole: { - if (column == 0) { - return "Overwrite"; - } else { - return QVariant(); - } - } break; - case Qt::CheckStateRole: { - if (column == 0) { - return Qt::Checked; - } else { - return QVariant(); - } - } break; - case Qt::TextAlignmentRole: return QVariant(Qt::AlignCenter | Qt::AlignVCenter); - case Qt::UserRole: return -1; - case Qt::ForegroundRole: return QBrush(Qt::red); - case Qt::ToolTipRole: return tr("This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit)"); - default: return QVariant(); - } -} - - -QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const -{ - switch (flag) { - case ModInfo::FLAG_BACKUP: return tr("Backup"); - case ModInfo::FLAG_SEPARATOR: return tr("Separator"); - case ModInfo::FLAG_INVALID: return tr("No valid game data"); - case ModInfo::FLAG_NOTENDORSED: return tr("Not endorsed yet"); - case ModInfo::FLAG_NOTES: { - QStringList output; - if (!modInfo->comments().isEmpty()) - output << QString("%1").arg(modInfo->comments()); - if (!modInfo->notes().isEmpty()) - output << QString("%1").arg(modInfo->notes()); - return output.join(""); - } - case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites loose files"); - case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten loose files"); - case ModInfo::FLAG_CONFLICT_MIXED: return tr("Loose files Overwrites & Overwritten"); - case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant"); - case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return tr("Overwrites an archive with loose files"); - case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return tr("Archive is overwritten by loose files"); - case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return tr("Overwrites another archive file"); - case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return tr("Overwritten by another archive file"); - case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return tr("Archive files overwrites & overwritten"); - case ModInfo::FLAG_ALTERNATE_GAME: return tr("
This mod is for a different game, " - "make sure it's compatible or it could cause crashes."); - default: return ""; - } -} - - -QVariantList ModList::contentsToIcons(const std::vector &contents) const -{ - QVariantList result; - std::set contentsSet(contents.begin(), contents.end()); - for (auto iter = m_ContentIcons.begin(); iter != m_ContentIcons.end(); ++iter) { - if (contentsSet.find(iter->first) != contentsSet.end()) { - result.append(std::get<0>(iter->second)); - } else { - result.append(QString()); - } - } - return result; -} - -QString ModList::contentsToToolTip(const std::vector &contents) const -{ - QString result(""); - - std::set contentsSet(contents.begin(), contents.end()); - for (auto iter = m_ContentIcons.begin(); iter != m_ContentIcons.end(); ++iter) { - if (contentsSet.find(iter->first) != contentsSet.end()) { - result.append(QString("" - "") - .arg(std::get<0>(iter->second)).arg(std::get<1>(iter->second))); - } - } - result.append("
%2
"); - return result; -} - - -QVariant ModList::data(const QModelIndex &modelIndex, int role) const -{ - if (m_Profile == nullptr) return QVariant(); - if (!modelIndex.isValid()) return QVariant(); - unsigned int modIndex = modelIndex.row(); - int column = modelIndex.column(); - - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - if ((role == Qt::DisplayRole) || - (role == Qt::EditRole)) { - if ((column == COL_FLAGS) - || (column == COL_CONTENT)) { - return QVariant(); - } else if (column == COL_NAME) { - auto flags = modInfo->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) - { - return modInfo->name().replace("_separator", ""); - } - else - return modInfo->name(); - } else if (column == COL_VERSION) { - VersionInfo verInfo = modInfo->getVersion(); - QString version = verInfo.displayString(); - if (role != Qt::EditRole) { - if (version.isEmpty() && modInfo->canBeUpdated()) { - version = "?"; - } - } - return version; - } else if (column == COL_PRIORITY) { - int priority = modInfo->getFixedPriority(); - if (priority != INT_MIN) { - return QVariant(); // hide priority for mods where it's fixed - } else { - return m_Profile->getModPriority(modIndex); - } - } else if (column == COL_MODID) { - int modID = modInfo->getNexusID(); - if (modID >= 0) { - return modID; - } - else { - return QVariant(); - } - } else if (column == COL_GAME) { - if (m_PluginContainer != nullptr) { - for (auto game : m_PluginContainer->plugins()) { - if (game->gameShortName().compare(modInfo->getGameName(), Qt::CaseInsensitive) == 0) - return game->gameName(); - } - } - return modInfo->getGameName(); - } else if (column == COL_CATEGORY) { - if (modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) { - return tr("Non-MO"); - } else { - int category = modInfo->getPrimaryCategory(); - if (category != -1) { - CategoryFactory &categoryFactory = CategoryFactory::instance(); - if (categoryFactory.categoryExists(category)) { - try { - int categoryIdx = categoryFactory.getCategoryIndex(category); - return categoryFactory.getCategoryName(categoryIdx); - } catch (const std::exception &e) { - qCritical("failed to retrieve category name: %s", e.what()); - return QString(); - } - } else { - qWarning("category %d doesn't exist (may have been removed)", category); - modInfo->setCategory(category, false); - return QString(); - } - } else { - return QVariant(); - } - } - } else if (column == COL_INSTALLTIME) { - // display installation time for mods that can be updated - if (modInfo->creationTime().isValid()) { - return modInfo->creationTime(); - } else { - return QVariant(); - } - } else if (column == COL_NOTES) { - return modInfo->comments(); - } else { - return tr("invalid"); - } - } else if ((role == Qt::CheckStateRole) && (column == 0)) { - if (modInfo->canBeEnabled()) { - return m_Profile->modEnabled(modIndex) ? Qt::Checked : Qt::Unchecked; - } else { - return QVariant(); - } - } else if (role == Qt::TextAlignmentRole) { - auto flags = modInfo->getFlags(); - if (column == COL_NAME) { - if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_CENTER) { - return QVariant(Qt::AlignCenter | Qt::AlignVCenter); - } else { - return QVariant(Qt::AlignLeft | Qt::AlignVCenter); - } - } else if (column == COL_VERSION) { - return QVariant(Qt::AlignRight | Qt::AlignVCenter); - } else if (column == COL_NOTES) { - return QVariant(Qt::AlignLeft | Qt::AlignVCenter); - } else { - return QVariant(Qt::AlignCenter | Qt::AlignVCenter); - } - } else if (role == Qt::UserRole) { - if (column == COL_CATEGORY) { - QVariantList categoryNames; - std::set categories = modInfo->getCategories(); - CategoryFactory &categoryFactory = CategoryFactory::instance(); - for (auto iter = categories.begin(); iter != categories.end(); ++iter) { - categoryNames.append(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*iter))); - } - if (categoryNames.count() != 0) { - return categoryNames; - } else { - return QVariant(); - } - } else if (column == COL_PRIORITY) { - int priority = modInfo->getFixedPriority(); - if (priority != INT_MIN) { - return priority; - } else { - return m_Profile->getModPriority(modIndex); - } - } else { - return modInfo->getNexusID(); - } - } else if (role == Qt::UserRole + 1) { - return modIndex; - } else if (role == Qt::UserRole + 2) { - switch (column) { - case COL_MODID: return QtGroupingProxy::AGGR_FIRST; - case COL_VERSION: return QtGroupingProxy::AGGR_MAX; - case COL_CATEGORY: return QtGroupingProxy::AGGR_FIRST; - case COL_PRIORITY: return QtGroupingProxy::AGGR_MIN; - default: return QtGroupingProxy::AGGR_NONE; - } - } else if (role == Qt::UserRole + 3) { - return contentsToIcons(modInfo->getContents()); - } else if (role == Qt::UserRole + 4) { - return modInfo->getGameName(); - } else if (role == Qt::FontRole) { - QFont result; - auto flags = modInfo->getFlags(); - if (column == COL_NAME) { - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) - { - //result.setCapitalization(QFont::AllUppercase); - result.setItalic(true); - //result.setUnderline(true); - result.setBold(true); - } else if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_INVALID) { - result.setItalic(true); - } - } else if ((column == COL_CATEGORY) && (modInfo->hasFlag(ModInfo::FLAG_FOREIGN))) { - result.setItalic(true); - } else if (column == COL_VERSION) { - if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) { - result.setWeight(QFont::Bold); - } - } - return result; - } else if (role == Qt::DecorationRole) { - if (column == COL_VERSION) { - if (modInfo->updateAvailable()) { - return QIcon(":/MO/gui/update_available"); - } else if (modInfo->downgradeAvailable()) { - return QIcon(":/MO/gui/warning"); - } else if (modInfo->getVersion().scheme() == VersionInfo::SCHEME_DATE) { - return QIcon(":/MO/gui/version_date"); - } - } - return QVariant(); - } else if (role == Qt::ForegroundRole) { - if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) && modInfo->getColor().isValid()) { - return Settings::getIdealTextColor(modInfo->getColor()); - } else if (column == COL_NAME) { - int highlight = modInfo->getHighlight(); - if (highlight & ModInfo::HIGHLIGHT_IMPORTANT) - return QBrush(Qt::darkRed); - else if (highlight & ModInfo::HIGHLIGHT_INVALID) - return QBrush(Qt::darkGray); - } else if (column == COL_VERSION) { - if (!modInfo->getNewestVersion().isValid()) { - return QVariant(); - } else if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) { - return QBrush(Qt::red); - } else { - return QBrush(Qt::darkGreen); - } - } - return QVariant(); - } else if ((role == Qt::BackgroundRole) - || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) { - bool overwrite = m_Overwrite.find(modIndex) != m_Overwrite.end(); - bool archiveOverwrite = m_ArchiveOverwrite.find(modIndex) != m_ArchiveOverwrite.end(); - bool archiveLooseOverwrite = m_ArchiveLooseOverwrite.find(modIndex) != m_ArchiveLooseOverwrite.end(); - bool overwritten = m_Overwritten.find(modIndex) != m_Overwritten.end(); - bool archiveOverwritten = m_ArchiveOverwritten.find(modIndex) != m_ArchiveOverwritten.end(); - bool archiveLooseOverwritten = m_ArchiveLooseOverwritten.find(modIndex) != m_ArchiveLooseOverwritten.end(); - if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_PLUGIN) { - return Settings::instance().modlistContainsPluginColor(); - } else if (overwritten || archiveLooseOverwritten) { - return Settings::instance().modlistOverwritingLooseColor(); - } else if (overwrite || archiveLooseOverwrite) { - return Settings::instance().modlistOverwrittenLooseColor(); - } else if (archiveOverwritten) { - return Settings::instance().modlistOverwritingArchiveColor(); - } else if (archiveOverwrite) { - return Settings::instance().modlistOverwrittenArchiveColor(); - } else if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) - && modInfo->getColor().isValid() - && ((role != ViewMarkingScrollBar::DEFAULT_ROLE) - || Settings::instance().colorSeparatorScrollbar())) { - return modInfo->getColor(); - } else { - return QVariant(); - } - } else if (role == Qt::ToolTipRole) { - if (column == COL_FLAGS) { - QString result; - - for (ModInfo::EFlag flag : modInfo->getFlags()) { - if (result.length() != 0) result += "
"; - result += getFlagText(flag, modInfo); - } - - return result; - } else if (column == COL_CONTENT) { - return contentsToToolTip(modInfo->getContents()); - } else if (column == COL_NAME) { - try { - return modInfo->getDescription(); - } catch (const std::exception &e) { - qCritical("invalid mod description: %s", e.what()); - return QString(); - } - } else if (column == COL_VERSION) { - QString text = tr("installed version: \"%1\", newest version: \"%2\"").arg(modInfo->getVersion().displayString(3)).arg(modInfo->getNewestVersion().displayString(3)); - if (modInfo->downgradeAvailable()) { - text += "
" + tr("The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn " - "(i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. " - "Either way you may want to \"upgrade\"."); - } - return text; - } else if (column == COL_CATEGORY) { - const std::set &categories = modInfo->getCategories(); - std::wostringstream categoryString; - categoryString << ToWString(tr("Categories:
")); - CategoryFactory &categoryFactory = CategoryFactory::instance(); - for (std::set::const_iterator catIter = categories.begin(); - catIter != categories.end(); ++catIter) { - if (catIter != categories.begin()) { - categoryString << " , "; - } - try { - categoryString << "" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << ""; - } catch (const std::exception &e) { - qCritical("failed to generate tooltip: %s", e.what()); - return QString(); - } - } - - return ToQString(categoryString.str()); - } else if (column == COL_NOTES) { - return getFlagText(ModInfo::FLAG_NOTES, modInfo); - } else { - return QVariant(); - } - } else { - return QVariant(); - } -} - - -bool ModList::renameMod(int index, const QString &newName) -{ - QString nameFixed = newName; - if (!fixDirectoryName(nameFixed) || nameFixed.isEmpty()) { - MessageDialog::showMessage(tr("Invalid name"), nullptr); - return false; - } - - if (ModList::allMods().contains(nameFixed, Qt::CaseInsensitive) && nameFixed.toLower()!=ModInfo::getByIndex(index)->name().toLower() ) { - MessageDialog::showMessage(tr("Name is already in use by another mod"), nullptr); - return false; - } - - ModInfo::Ptr modInfo = ModInfo::getByIndex(index); - QString oldName = modInfo->name(); - if (nameFixed != oldName) { - // before we rename, ensure there is no scheduled asynchronous to rewrite - m_Profile->cancelModlistWrite(); - - - if (modInfo->setName(nameFixed)) - // Notice there is a good chance that setName() updated the modinfo indexes - // the modRenamed() call will refresh the indexes in the current profile - // and update the modlists in all profiles - emit modRenamed(oldName, nameFixed); - } - - // invalidate the currently displayed state of this list - notifyChange(-1); - return true; -} - -bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (m_Profile == nullptr) return false; - - if (static_cast(index.row()) >= ModInfo::getNumMods()) { - return false; - } - - int modID = index.row(); - - ModInfo::Ptr info = ModInfo::getByIndex(modID); - IModList::ModStates oldState = state(modID); - - bool result = false; - - emit aboutToChangeData(); - - if (role == Qt::CheckStateRole) { - bool enabled = value.toInt() == Qt::Checked; - if (m_Profile->modEnabled(modID) != enabled) { - m_Profile->setModEnabled(modID, enabled); - m_Modified = true; - m_LastCheck.restart(); - emit modlistChanged(index, role); - } - result = true; - emit dataChanged(index, index); - } else if (role == Qt::EditRole) { - switch (index.column()) { - case COL_NAME: { - auto flags = info->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) - { - result = renameMod(modID, value.toString() + "_separator"); - } - else - result = renameMod(modID, value.toString()); - } break; - case COL_PRIORITY: { - bool ok = false; - int newPriority = value.toInt(&ok); - if (ok && newPriority < 0) { - newPriority = 0; - } - if (ok) { - m_Profile->setModPriority(modID, newPriority); - - emit modorder_changed(); - result = true; - } else { - result = false; - } - } break; - case COL_MODID: { - bool ok = false; - int newID = value.toInt(&ok); - if (ok) { - info->setNexusID(newID); - emit modlistChanged(index, role); - result = true; - } else { - result = false; - } - } break; - case COL_VERSION: { - VersionInfo::VersionScheme scheme = info->getVersion().scheme(); - VersionInfo version(value.toString(), scheme, true); - if (version.isValid()) { - info->setVersion(version); - result = true; - } else { - result = false; - } - } break; - case COL_NOTES: { - info->setComments(value.toString()); - result = true; - } break; - default: { - qWarning("edit on column \"%s\" not supported", - getColumnName(index.column()).toUtf8().constData()); - result = false; - } break; - } - if (result) { - emit dataChanged(index, index); - } - } - - emit postDataChanged(); - - IModList::ModStates newState = state(modID); - if (oldState != newState) { - try { - m_ModStateChanged(info->name(), newState); - } catch (const std::exception &e) { - qCritical("failed to invoke state changed notification: %s", e.what()); - } catch (...) { - qCritical("failed to invoke state changed notification: unknown exception"); - } - } - - return result; -} - - -QVariant ModList::headerData(int section, Qt::Orientation orientation, - int role) const -{ - if (orientation == Qt::Horizontal) { - if (role == Qt::DisplayRole) { - return getColumnName(section); - } else if (role == Qt::ToolTipRole) { - return getColumnToolTip(section); - } else if (role == Qt::TextAlignmentRole) { - return QVariant(Qt::AlignCenter); - } else if (role == Qt::SizeHintRole) { - QSize temp = m_FontMetrics.size(Qt::TextSingleLine, getColumnName(section)); - temp.rwidth() += 20; - temp.rheight() += 12; - return temp; - } - } - return QAbstractItemModel::headerData(section, orientation, role); -} - - -Qt::ItemFlags ModList::flags(const QModelIndex &modelIndex) const -{ - Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex); - if (modelIndex.internalId() < 0) { - return Qt::ItemIsEnabled; - } - if (modelIndex.isValid()) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modelIndex.row()); - std::vector flags = modInfo->getFlags(); - if (modInfo->getFixedPriority() == INT_MIN) { - result |= Qt::ItemIsDragEnabled; - result |= Qt::ItemIsUserCheckable; - if ((modelIndex.column() == COL_PRIORITY) || - (modelIndex.column() == COL_VERSION) || - (modelIndex.column() == COL_MODID)) { - result |= Qt::ItemIsEditable; - } - if (((modelIndex.column() == COL_NAME) || - (modelIndex.column() == COL_NOTES)) - && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end())) { - result |= Qt::ItemIsEditable; - } - } - if (m_DropOnItems - && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end())) { - result |= Qt::ItemIsDropEnabled; - } - } else { - if (!m_DropOnItems) result |= Qt::ItemIsDropEnabled; - } - return result; -} - - -QStringList ModList::mimeTypes() const -{ - QStringList result = QAbstractItemModel::mimeTypes(); - result.append("text/uri-list"); - return result; -} - -QMimeData *ModList::mimeData(const QModelIndexList &indexes) const -{ - QMimeData *result = QAbstractItemModel::mimeData(indexes); - result->setData("text/plain", "mod"); - return result; -} - -void ModList::changeModPriority(std::vector sourceIndices, int newPriority) -{ - if (m_Profile == nullptr) return; - - emit layoutAboutToBeChanged(); - Profile *profile = m_Profile; - - // sort the moving mods by ascending priorities - std::sort(sourceIndices.begin(), sourceIndices.end(), - [profile](const int &LHS, const int &RHS) { - return profile->getModPriority(LHS) > profile->getModPriority(RHS); - }); - - // move mods that are decreasing in priority - for (std::vector::const_iterator iter = sourceIndices.begin(); - iter != sourceIndices.end(); ++iter) { - int oldPriority = profile->getModPriority(*iter); - if (oldPriority > newPriority) { - profile->setModPriority(*iter, newPriority); - m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority); - } - } - - // sort the moving mods by descending priorities - std::sort(sourceIndices.begin(), sourceIndices.end(), - [profile](const int &LHS, const int &RHS) { - return profile->getModPriority(LHS) < profile->getModPriority(RHS); - }); - - // if at least one mod is increasing in priority, the target index is - // that of the row BELOW the dropped location, otherwise it's the one above - for (std::vector::const_iterator iter = sourceIndices.begin(); - iter != sourceIndices.end(); ++iter) { - int oldPriority = profile->getModPriority(*iter); - if (oldPriority < newPriority) { - --newPriority; - break; - } - } - - // move mods that are increasing in priority - for (std::vector::const_iterator iter = sourceIndices.begin(); - iter != sourceIndices.end(); ++iter) { - int oldPriority = profile->getModPriority(*iter); - if (oldPriority < newPriority) { - profile->setModPriority(*iter, newPriority); - m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority); - } - } - - emit layoutChanged(); - - emit modorder_changed(); -} - - -void ModList::changeModPriority(int sourceIndex, int newPriority) -{ - if (m_Profile == nullptr) return; - emit layoutAboutToBeChanged(); - - m_Profile->setModPriority(sourceIndex, newPriority); - - emit layoutChanged(); - - emit modorder_changed(); -} - -void ModList::setOverwriteMarkers(const std::set &overwrite, const std::set &overwritten) -{ - m_Overwrite = overwrite; - m_Overwritten = overwritten; - notifyChange(0, rowCount() - 1); -} - -void ModList::setArchiveOverwriteMarkers(const std::set &overwrite, const std::set &overwritten) -{ - m_ArchiveOverwrite = overwrite; - m_ArchiveOverwritten = overwritten; - notifyChange(0, rowCount() - 1); -} - -void ModList::setArchiveLooseOverwriteMarkers(const std::set &overwrite, const std::set &overwritten) -{ - m_ArchiveLooseOverwrite = overwrite; - m_ArchiveLooseOverwritten = overwritten; - notifyChange(0, rowCount() - 1); -} - -void ModList::setPluginContainer(PluginContainer *pluginContianer) -{ - m_PluginContainer = pluginContianer; -} - -bool ModList::modInfoAboutToChange(ModInfo::Ptr info) -{ - if (m_ChangeInfo.name.isEmpty()) { - m_ChangeInfo.name = info->name(); - m_ChangeInfo.state = state(info->name()); - return true; - } else { - return false; - } -} - -void ModList::modInfoChanged(ModInfo::Ptr info) -{ - if (info->name() == m_ChangeInfo.name) { - IModList::ModStates newState = state(info->name()); - if (m_ChangeInfo.state != newState) { - m_ModStateChanged(info->name(), newState); - } - - int row = ModInfo::getIndex(info->name()); - info->testValid(); - emit aboutToChangeData(); - emit dataChanged(index(row, 0), index(row, columnCount())); - emit postDataChanged(); - } else { - qCritical("modInfoChanged not called after modInfoAboutToChange"); - } - m_ChangeInfo.name = QString(); -} - -void ModList::disconnectSlots() { - m_ModMoved.disconnect_all_slots(); - m_ModStateChanged.disconnect_all_slots(); -} - -int ModList::timeElapsedSinceLastChecked() const -{ - return m_LastCheck.elapsed(); -} - -void ModList::highlightMods(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry) -{ - for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { - ModInfo::getByIndex(i)->setPluginSelected(false); - } - for (QModelIndex idx : selection->selectedRows(PluginList::COL_NAME)) { - QString modName = idx.data().toString(); - - const MOShared::FileEntry::Ptr fileEntry = directoryEntry.findFile(modName.toStdWString()); - if (fileEntry.get() != nullptr) { - bool archive = false; - std::vector>> origins; - { - std::vector>> alternatives = fileEntry->getAlternatives(); - origins.insert(origins.end(), std::pair>(fileEntry->getOrigin(archive), fileEntry->getArchive())); - } - for (auto originInfo : origins) { - MOShared::FilesOrigin &origin = directoryEntry.getOriginByID(originInfo.first); - for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { - if (ModInfo::getByIndex(i)->internalName() == QString::fromStdWString(origin.getName())) { - ModInfo::getByIndex(i)->setPluginSelected(true); - break; - } - } - } - } - } - notifyChange(0, rowCount() - 1); -} - -IModList::ModStates ModList::state(unsigned int modIndex) const -{ - IModList::ModStates result; - if (modIndex != UINT_MAX) { - result |= IModList::STATE_EXISTS; - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - if (modInfo->isEmpty()) { - result |= IModList::STATE_EMPTY; - } - if (modInfo->endorsedState() == ModInfo::ENDORSED_TRUE) { - result |= IModList::STATE_ENDORSED; - } - if (modInfo->isValid()) { - result |= IModList::STATE_VALID; - } - if (modInfo->isRegular()) { - QSharedPointer modInfoRegular = modInfo.staticCast(); - if (modInfoRegular->isAlternate() && !modInfoRegular->isConverted()) - result |= IModList::STATE_ALTERNATE; - if (!modInfo->isValid() && modInfoRegular->isValidated()) - result |= IModList::STATE_VALID; - } - if (modInfo->canBeEnabled()) { - if (m_Profile->modEnabled(modIndex)) { - result |= IModList::STATE_ACTIVE; - } - } else { - result |= IModList::STATE_ESSENTIAL; - } - } - return result; -} - -QString ModList::displayName(const QString &internalName) const -{ - unsigned int modIndex = ModInfo::getIndex(internalName); - if (modIndex == UINT_MAX) { - // might be better to throw an exception? - return internalName; - } else { - return ModInfo::getByIndex(modIndex)->name(); - } -} - -QStringList ModList::allMods() const -{ - QStringList result; - for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { - result.append(ModInfo::getByIndex(i)->internalName()); - } - return result; -} - -IModList::ModStates ModList::state(const QString &name) const -{ - unsigned int modIndex = ModInfo::getIndex(name); - - return state(modIndex); -} - -bool ModList::setActive(const QString &name, bool active) -{ - unsigned int modIndex = ModInfo::getIndex(name); - if (modIndex == UINT_MAX) { - return false; - } else { - m_Profile->setModEnabled(modIndex, active); - - IModList::ModStates newState = state(modIndex); - m_ModStateChanged(name, newState); - return true; - } -} - -int ModList::priority(const QString &name) const -{ - unsigned int modIndex = ModInfo::getIndex(name); - if (modIndex == UINT_MAX) { - return -1; - } else { - return m_Profile->getModPriority(modIndex); - } -} - -bool ModList::setPriority(const QString &name, int newPriority) -{ - if ((newPriority < 0) || (newPriority >= static_cast(m_Profile->numRegularMods()))) { - return false; - } - - unsigned int modIndex = ModInfo::getIndex(name); - if (modIndex == UINT_MAX) { - return false; - } else { - m_Profile->setModPriority(modIndex, newPriority); - notifyChange(modIndex); - return true; - } -} - -bool ModList::onModStateChanged(const std::function &func) -{ - auto conn = m_ModStateChanged.connect(func); - return conn.connected(); -} - -bool ModList::onModMoved(const std::function &func) -{ - auto conn = m_ModMoved.connect(func); - return conn.connected(); -} - -bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &parent) -{ - QStringList source; - QStringList target; - - if (row == -1) { - row = parent.row(); - } - ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - QDir modDirectory(modInfo->absolutePath()); - QDir gameDirectory(Settings::instance().getOverwriteDirectory()); - - unsigned int overwriteIndex = ModInfo::findMod([] (ModInfo::Ptr mod) -> bool { - std::vector flags = mod->getFlags(); - return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); }); - - QString overwriteName = ModInfo::getByIndex(overwriteIndex)->name(); - - for (const QUrl &url : mimeData->urls()) { - //qDebug("URL drop requested: %s", qUtf8Printable(url.toLocalFile())); - if (!url.isLocalFile()) { - qDebug("URL drop ignored: Not a local file."); - continue; - } - QString relativePath = gameDirectory.relativeFilePath(url.toLocalFile()); - if (relativePath.startsWith("..")) { - qDebug("URL drop ignored: relative path starts with .."); - continue; - } - source.append(url.toLocalFile()); - target.append(modDirectory.absoluteFilePath(relativePath)); - emit fileMoved(relativePath, overwriteName, modInfo->name()); - } - - if (source.count() != 0) { - if (!shellMove(source, target)) { - qDebug("Move failed %lu",::GetLastError()); - return false; - } - } - - if (!modInfo->isValid()) { - modInfo->testValid(); - } - - return true; -} - -bool ModList::dropMod(const QMimeData *mimeData, int row, const QModelIndex &parent) -{ - - try { - QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist"); - QDataStream stream(&encoded, QIODevice::ReadOnly); - std::vector sourceRows; - - while (!stream.atEnd()) { - int sourceRow, col; - QMap roleDataMap; - stream >> sourceRow >> col >> roleDataMap; - if (col == 0) { - sourceRows.push_back(sourceRow); - } - } - - if (row == -1) { - row = parent.row(); - } - - if ((row < 0) || (static_cast(row) >= ModInfo::getNumMods())) { - return false; - } - - int newPriority = 0; - { - if ((row < 0) || (row > static_cast(m_Profile->numRegularMods()))) { - newPriority = m_Profile->numRegularMods() + 1; - } else { - newPriority = m_Profile->getModPriority(row); - } - if (newPriority == -1) { - newPriority = m_Profile->numRegularMods() + 1; - } - } - changeModPriority(sourceRows, newPriority); - } catch (const std::exception &e) { - reportError(tr("drag&drop failed: %1").arg(e.what())); - } - - return false; -} - - -bool ModList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent) -{ - if (action == Qt::IgnoreAction) { - return true; - } - - if (m_Profile == nullptr) return false; - - if (mimeData->hasUrls()) { - return dropURLs(mimeData, row, parent); - } else if (mimeData->hasText()) { - return dropMod(mimeData, row, parent); - } else { - return false; - } -} - -void ModList::removeRowForce(int row, const QModelIndex &parent) -{ - if (static_cast(row) >= ModInfo::getNumMods()) { - return; - } - if (m_Profile == nullptr) return; - - ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - - bool wasEnabled = m_Profile->modEnabled(row); - - m_Profile->setModEnabled(row, false); - - m_Profile->cancelModlistWrite(); - beginRemoveRows(parent, row, row); - ModInfo::removeMod(row); - endRemoveRows(); - m_Profile->refreshModStatus(); // removes the mod from the status list - m_Profile->writeModlist(); // this ensures the modified list gets written back before new mods can be installed - - if (wasEnabled) { - emit removeOrigin(modInfo->name()); - } - - emit modUninstalled(modInfo->getInstallationFile()); -} - -bool ModList::removeRows(int row, int count, const QModelIndex &parent) -{ - if (static_cast(row) >= ModInfo::getNumMods()) { - return false; - } - if (m_Profile == nullptr) { - return false; - } - - bool success = false; - - if (count == 1) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - std::vector flags = modInfo->getFlags(); - if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) && (QDir(modInfo->absolutePath()).count() > 2)) { - emit clearOverwrite(); - success = true; - } - } - - for (int i = 0; i < count; ++i) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(row + i); - if (!modInfo->isRegular()) { - continue; - } - - success = true; - - QMessageBox confirmBox(QMessageBox::Question, tr("Confirm"), - tr("Are you sure you want to remove \"%1\"?").arg(modInfo->name()), - QMessageBox::Yes | QMessageBox::No); - - if (confirmBox.exec() == QMessageBox::Yes) { - m_Profile->setModEnabled(row + i, false); - removeRowForce(row + i, parent); - } - } - - return success; -} - - -void ModList::notifyChange(int rowStart, int rowEnd) -{ - if (rowStart < 0) { - m_Overwrite.clear(); - m_Overwritten.clear(); - m_ArchiveOverwrite.clear(); - m_ArchiveOverwritten.clear(); - m_ArchiveLooseOverwrite.clear(); - m_ArchiveLooseOverwritten.clear(); - beginResetModel(); - endResetModel(); - } else { - if (rowEnd == -1) { - rowEnd = rowStart; - } - emit dataChanged(this->index(rowStart, 0), this->index(rowEnd, this->columnCount() - 1)); - } -} - - -QModelIndex ModList::index(int row, int column, const QModelIndex&) const -{ - if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) { - return QModelIndex(); - } - QModelIndex res = createIndex(row, column, row); - return res; -} - - -QModelIndex ModList::parent(const QModelIndex&) const -{ - return QModelIndex(); -} - -QMap ModList::itemData(const QModelIndex &index) const -{ - QMap result = QAbstractItemModel::itemData(index); - result[Qt::UserRole] = data(index, Qt::UserRole); - return result; -} - - -void ModList::dropModeUpdate(bool dropOnItems) -{ - if (m_DropOnItems != dropOnItems) { - m_DropOnItems = dropOnItems; - } -} - - -QString ModList::getColumnName(int column) -{ - switch (column) { - case COL_FLAGS: return tr("Flags"); - case COL_CONTENT: return tr("Content"); - case COL_NAME: return tr("Mod Name"); - case COL_VERSION: return tr("Version"); - case COL_PRIORITY: return tr("Priority"); - case COL_CATEGORY: return tr("Category"); - case COL_GAME: return tr("Source Game"); - case COL_MODID: return tr("Nexus ID"); - case COL_INSTALLTIME: return tr("Installation"); - case COL_NOTES: return tr("Notes"); - default: return tr("unknown"); - } -} - - -QString ModList::getColumnToolTip(int column) -{ - switch (column) { - case COL_NAME: return tr("Name of your mods"); - case COL_VERSION: return tr("Version of the mod (if available)"); - case COL_PRIORITY: return tr("Installation priority of your mod. The higher, the more \"important\" it is and thus " - "overwrites files from mods with lower priority."); - case COL_CATEGORY: return tr("Category of the mod."); - case COL_GAME: return tr("The source game which was the origin of this mod."); - case COL_MODID: return tr("Id of the mod as used on Nexus."); - case COL_FLAGS: return tr("Emblemes to highlight things that might require attention."); - case COL_CONTENT: return tr("Depicts the content of the mod:
" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - - "
Game plugins (esp/esm/esl)
Interface
Meshes
BSA
Textures
Sounds
Music
Strings
Scripts (Papyrus)
Script Extender plugins
SkyProc Patcher
Mod Configuration Menu
INI files
ModGroup files
"); - case COL_INSTALLTIME: return tr("Time this mod was installed"); - case COL_NOTES: return tr("User notes about the mod"); - default: return tr("unknown"); - } -} - - -bool ModList::moveSelection(QAbstractItemView *itemView, int direction) -{ - QItemSelectionModel *selectionModel = itemView->selectionModel(); - - const QAbstractProxyModel *proxyModel = qobject_cast(selectionModel->model()); - const QSortFilterProxyModel *filterModel = nullptr; - - while ((filterModel == nullptr) && (proxyModel != nullptr)) { - filterModel = qobject_cast(proxyModel); - if (filterModel == nullptr) { - proxyModel = qobject_cast(proxyModel->sourceModel()); - } - } - if (filterModel == nullptr) { - return true; - } - - int offset = -1; - if (((direction < 0) && (filterModel->sortOrder() == Qt::DescendingOrder)) || - ((direction > 0) && (filterModel->sortOrder() == Qt::AscendingOrder))) { - offset = 1; - } - - QModelIndexList rows = selectionModel->selectedRows(); - if (direction > 0) { - for (int i = 0; i < rows.size() / 2; ++i) { - rows.swap(i, rows.size() - i - 1); - } - } - for (QModelIndex idx : rows) { - if (filterModel != nullptr) { - idx = filterModel->mapToSource(idx); - } - int newPriority = m_Profile->getModPriority(idx.row()) + offset; - if ((newPriority >= 0) && (newPriority < static_cast(m_Profile->numRegularMods()))) { - m_Profile->setModPriority(idx.row(), newPriority); - notifyChange(idx.row()); - } - } - emit modorder_changed(); - return true; -} - -bool ModList::deleteSelection(QAbstractItemView *itemView) -{ - QItemSelectionModel *selectionModel = itemView->selectionModel(); - - QModelIndexList rows = selectionModel->selectedRows(); - if (rows.count() > 1) { - emit removeSelectedMods(); - } else if (rows.count() == 1) { - removeRow(rows[0].data(Qt::UserRole + 1).toInt(), QModelIndex()); - } - return true; -} - -bool ModList::toggleSelection(QAbstractItemView *itemView) -{ - emit aboutToChangeData(); - - QItemSelectionModel *selectionModel = itemView->selectionModel(); - - QList modsToEnable; - QList modsToDisable; - QModelIndexList dirtyMods; - for (QModelIndex idx : selectionModel->selectedRows()) { - int modId = idx.data(Qt::UserRole + 1).toInt(); - if (m_Profile->modEnabled(modId)) { - modsToDisable.append(modId); - dirtyMods.append(idx); - } else { - modsToEnable.append(modId); - dirtyMods.append(idx); - } - } - - m_Profile->setModsEnabled(modsToEnable, modsToDisable); - - emit modlistChanged(dirtyMods, 0); - - m_Modified = true; - m_LastCheck.restart(); - - emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); - - emit postDataChanged(); - - return true; -} - -bool ModList::eventFilter(QObject *obj, QEvent *event) -{ - if (event->type() == QEvent::ContextMenu) { - QContextMenuEvent *contextEvent = static_cast(event); - QWidget *object = qobject_cast(obj); - if ((object != nullptr) && (contextEvent->reason() == QContextMenuEvent::Mouse)) { - emit requestColumnSelect(object->mapToGlobal(contextEvent->pos())); - - return true; - } - } else if ((event->type() == QEvent::KeyPress) && (m_Profile != nullptr)) { - QAbstractItemView *itemView = qobject_cast(obj); - QKeyEvent *keyEvent = static_cast(event); - - if ((itemView != nullptr) - && (keyEvent->modifiers() == Qt::ControlModifier) - && ((keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down))) { - return moveSelection(itemView, keyEvent->key() == Qt::Key_Up ? -1 : 1); - } else if (keyEvent->key() == Qt::Key_Delete) { - return deleteSelection(itemView); - } else if (keyEvent->key() == Qt::Key_Space) { - return toggleSelection(itemView); - } - return QAbstractItemModel::eventFilter(obj, event); - } - return QAbstractItemModel::eventFilter(obj, event); -} - -//note: caller needs to make sure sort proxy is updated -void ModList::enableSelected(const QItemSelectionModel *selectionModel) -{ - if (selectionModel->hasSelection()) { - QList modsToEnable; - for (auto row : selectionModel->selectedRows(COL_PRIORITY)) { - int modID = m_Profile->modIndexByPriority(row.data().toInt()); - modsToEnable.append(modID); - } - m_Profile->setModsEnabled(modsToEnable, QList()); - } -} - -//note: caller needs to make sure sort proxy is updated -void ModList::disableSelected(const QItemSelectionModel *selectionModel) -{ - if (selectionModel->hasSelection()) { - QList modsToDisable; - for (auto row : selectionModel->selectedRows(COL_PRIORITY)) { - int modID = m_Profile->modIndexByPriority(row.data().toInt()); - modsToDisable.append(modID); - } - m_Profile->setModsEnabled(QList(), modsToDisable); - } -} +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "modlist.h" + +#include "messagedialog.h" +#include "installationtester.h" +#include "qtgroupingproxy.h" +#include "viewmarkingscrollbar.h" +#include "modlistsortproxy.h" +#include "pluginlist.h" +#include "settings.h" +#include "modinforegular.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +using namespace MOBase; + + +ModList::ModList(PluginContainer *pluginContainer, QObject *parent) + : QAbstractItemModel(parent) + , m_Profile(nullptr) + , m_NexusInterface(nullptr) + , m_Modified(false) + , m_FontMetrics(QFont()) + , m_DropOnItems(false) + , m_PluginContainer(pluginContainer) +{ + m_ContentIcons[ModInfo::CONTENT_PLUGIN] = std::make_tuple(":/MO/gui/content/plugin", tr("Game Plugins (ESP/ESM/ESL)")); + m_ContentIcons[ModInfo::CONTENT_INTERFACE] = std::make_tuple(":/MO/gui/content/interface", tr("Interface")); + m_ContentIcons[ModInfo::CONTENT_MESH] = std::make_tuple(":/MO/gui/content/mesh", tr("Meshes")); + m_ContentIcons[ModInfo::CONTENT_BSA] = std::make_tuple(":/MO/gui/content/bsa", tr("Bethesda Archive")); + m_ContentIcons[ModInfo::CONTENT_SCRIPT] = std::make_tuple(":/MO/gui/content/script", tr("Scripts (Papyrus)")); + m_ContentIcons[ModInfo::CONTENT_SKSE] = std::make_tuple(":/MO/gui/content/skse", tr("Script Extender Plugin")); + m_ContentIcons[ModInfo::CONTENT_SKYPROC] = std::make_tuple(":/MO/gui/content/skyproc", tr("SkyProc Patcher")); + m_ContentIcons[ModInfo::CONTENT_SOUND] = std::make_tuple(":/MO/gui/content/sound", tr("Sound or Music")); + m_ContentIcons[ModInfo::CONTENT_TEXTURE] = std::make_tuple(":/MO/gui/content/texture", tr("Textures")); + m_ContentIcons[ModInfo::CONTENT_MCM] = std::make_tuple(":/MO/gui/content/menu", tr("MCM Configuration")); + m_ContentIcons[ModInfo::CONTENT_INI] = std::make_tuple(":/MO/gui/content/inifile", tr("INI files")); + m_ContentIcons[ModInfo::CONTENT_MODGROUP] = std::make_tuple(":/MO/gui/content/modgroup", tr("ModGroup files")); + + m_LastCheck.start(); +} + +ModList::~ModList() +{ + m_ModStateChanged.disconnect_all_slots(); + m_ModMoved.disconnect_all_slots(); +} + +void ModList::setProfile(Profile *profile) +{ + m_Profile = profile; +} + +int ModList::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return ModInfo::getNumMods(); + } else { + return 0; + } +} + +bool ModList::hasChildren(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return ModInfo::getNumMods() > 0; + } else { + return false; + } +} + + +int ModList::columnCount(const QModelIndex &) const +{ + return COL_LASTCOLUMN + 1; +} + + +QVariant ModList::getOverwriteData(int column, int role) const +{ + switch (role) { + case Qt::DisplayRole: { + if (column == 0) { + return "Overwrite"; + } else { + return QVariant(); + } + } break; + case Qt::CheckStateRole: { + if (column == 0) { + return Qt::Checked; + } else { + return QVariant(); + } + } break; + case Qt::TextAlignmentRole: return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + case Qt::UserRole: return -1; + case Qt::ForegroundRole: return QBrush(Qt::red); + case Qt::ToolTipRole: return tr("This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit)"); + default: return QVariant(); + } +} + + +QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const +{ + switch (flag) { + case ModInfo::FLAG_BACKUP: return tr("Backup"); + case ModInfo::FLAG_SEPARATOR: return tr("Separator"); + case ModInfo::FLAG_INVALID: return tr("No valid game data"); + case ModInfo::FLAG_NOTENDORSED: return tr("Not endorsed yet"); + case ModInfo::FLAG_NOTES: { + QStringList output; + if (!modInfo->comments().isEmpty()) + output << QString("%1").arg(modInfo->comments()); + if (!modInfo->notes().isEmpty()) + output << QString("%1").arg(modInfo->notes()); + return output.join(""); + } + case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites loose files"); + case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten loose files"); + case ModInfo::FLAG_CONFLICT_MIXED: return tr("Loose files Overwrites & Overwritten"); + case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant"); + case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return tr("Overwrites an archive with loose files"); + case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return tr("Archive is overwritten by loose files"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return tr("Overwrites another archive file"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return tr("Overwritten by another archive file"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return tr("Archive files overwrites & overwritten"); + case ModInfo::FLAG_ALTERNATE_GAME: return tr("
This mod is for a different game, " + "make sure it's compatible or it could cause crashes."); + default: return ""; + } +} + + +QVariantList ModList::contentsToIcons(const std::vector &contents) const +{ + QVariantList result; + std::set contentsSet(contents.begin(), contents.end()); + for (auto iter = m_ContentIcons.begin(); iter != m_ContentIcons.end(); ++iter) { + if (contentsSet.find(iter->first) != contentsSet.end()) { + result.append(std::get<0>(iter->second)); + } else { + result.append(QString()); + } + } + return result; +} + +QString ModList::contentsToToolTip(const std::vector &contents) const +{ + QString result(""); + + std::set contentsSet(contents.begin(), contents.end()); + for (auto iter = m_ContentIcons.begin(); iter != m_ContentIcons.end(); ++iter) { + if (contentsSet.find(iter->first) != contentsSet.end()) { + result.append(QString("" + "") + .arg(std::get<0>(iter->second)).arg(std::get<1>(iter->second))); + } + } + result.append("
%2
"); + return result; +} + + +QVariant ModList::data(const QModelIndex &modelIndex, int role) const +{ + if (m_Profile == nullptr) return QVariant(); + if (!modelIndex.isValid()) return QVariant(); + unsigned int modIndex = modelIndex.row(); + int column = modelIndex.column(); + + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + if ((role == Qt::DisplayRole) || + (role == Qt::EditRole)) { + if ((column == COL_FLAGS) + || (column == COL_CONTENT)) { + return QVariant(); + } else if (column == COL_NAME) { + auto flags = modInfo->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) + { + return modInfo->name().replace("_separator", ""); + } + else + return modInfo->name(); + } else if (column == COL_VERSION) { + VersionInfo verInfo = modInfo->getVersion(); + QString version = verInfo.displayString(); + if (role != Qt::EditRole) { + if (version.isEmpty() && modInfo->canBeUpdated()) { + version = "?"; + } + } + return version; + } else if (column == COL_PRIORITY) { + int priority = modInfo->getFixedPriority(); + if (priority != INT_MIN) { + return QVariant(); // hide priority for mods where it's fixed + } else { + return m_Profile->getModPriority(modIndex); + } + } else if (column == COL_MODID) { + int modID = modInfo->getNexusID(); + if (modID >= 0) { + return modID; + } + else { + return QVariant(); + } + } else if (column == COL_GAME) { + if (m_PluginContainer != nullptr) { + for (auto game : m_PluginContainer->plugins()) { + if (game->gameShortName().compare(modInfo->getGameName(), Qt::CaseInsensitive) == 0) + return game->gameName(); + } + } + return modInfo->getGameName(); + } else if (column == COL_CATEGORY) { + if (modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) { + return tr("Non-MO"); + } else { + int category = modInfo->getPrimaryCategory(); + if (category != -1) { + CategoryFactory &categoryFactory = CategoryFactory::instance(); + if (categoryFactory.categoryExists(category)) { + try { + int categoryIdx = categoryFactory.getCategoryIndex(category); + return categoryFactory.getCategoryName(categoryIdx); + } catch (const std::exception &e) { + qCritical("failed to retrieve category name: %s", e.what()); + return QString(); + } + } else { + qWarning("category %d doesn't exist (may have been removed)", category); + modInfo->setCategory(category, false); + return QString(); + } + } else { + return QVariant(); + } + } + } else if (column == COL_INSTALLTIME) { + // display installation time for mods that can be updated + if (modInfo->creationTime().isValid()) { + return modInfo->creationTime(); + } else { + return QVariant(); + } + } else if (column == COL_NOTES) { + return modInfo->comments(); + } else { + return tr("invalid"); + } + } else if ((role == Qt::CheckStateRole) && (column == 0)) { + if (modInfo->canBeEnabled()) { + return m_Profile->modEnabled(modIndex) ? Qt::Checked : Qt::Unchecked; + } else { + return QVariant(); + } + } else if (role == Qt::TextAlignmentRole) { + auto flags = modInfo->getFlags(); + if (column == COL_NAME) { + if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_CENTER) { + return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + } else { + return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + } + } else if (column == COL_VERSION) { + return QVariant(Qt::AlignRight | Qt::AlignVCenter); + } else if (column == COL_NOTES) { + return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + } else { + return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + } + } else if (role == Qt::UserRole) { + if (column == COL_CATEGORY) { + QVariantList categoryNames; + std::set categories = modInfo->getCategories(); + CategoryFactory &categoryFactory = CategoryFactory::instance(); + for (auto iter = categories.begin(); iter != categories.end(); ++iter) { + categoryNames.append(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*iter))); + } + if (categoryNames.count() != 0) { + return categoryNames; + } else { + return QVariant(); + } + } else if (column == COL_PRIORITY) { + int priority = modInfo->getFixedPriority(); + if (priority != INT_MIN) { + return priority; + } else { + return m_Profile->getModPriority(modIndex); + } + } else { + return modInfo->getNexusID(); + } + } else if (role == Qt::UserRole + 1) { + return modIndex; + } else if (role == Qt::UserRole + 2) { + switch (column) { + case COL_MODID: return QtGroupingProxy::AGGR_FIRST; + case COL_VERSION: return QtGroupingProxy::AGGR_MAX; + case COL_CATEGORY: return QtGroupingProxy::AGGR_FIRST; + case COL_PRIORITY: return QtGroupingProxy::AGGR_MIN; + default: return QtGroupingProxy::AGGR_NONE; + } + } else if (role == Qt::UserRole + 3) { + return contentsToIcons(modInfo->getContents()); + } else if (role == Qt::UserRole + 4) { + return modInfo->getGameName(); + } else if (role == Qt::FontRole) { + QFont result; + auto flags = modInfo->getFlags(); + if (column == COL_NAME) { + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) + { + //result.setCapitalization(QFont::AllUppercase); + result.setItalic(true); + //result.setUnderline(true); + result.setBold(true); + } else if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_INVALID) { + result.setItalic(true); + } + } else if ((column == COL_CATEGORY) && (modInfo->hasFlag(ModInfo::FLAG_FOREIGN))) { + result.setItalic(true); + } else if (column == COL_VERSION) { + if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) { + result.setWeight(QFont::Bold); + } + if (modInfo->canBeUpdated()) { + result.setItalic(true); + } + } + return result; + } else if (role == Qt::DecorationRole) { + if (column == COL_VERSION) { + if (modInfo->updateAvailable()) { + return QIcon(":/MO/gui/update_available"); + } else if (modInfo->downgradeAvailable()) { + return QIcon(":/MO/gui/warning"); + } else if (modInfo->getVersion().scheme() == VersionInfo::SCHEME_DATE) { + return QIcon(":/MO/gui/version_date"); + } + } + return QVariant(); + } else if (role == Qt::ForegroundRole) { + if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) && modInfo->getColor().isValid()) { + return Settings::getIdealTextColor(modInfo->getColor()); + } else if (column == COL_NAME) { + int highlight = modInfo->getHighlight(); + if (highlight & ModInfo::HIGHLIGHT_IMPORTANT) + return QBrush(Qt::darkRed); + else if (highlight & ModInfo::HIGHLIGHT_INVALID) + return QBrush(Qt::darkGray); + } else if (column == COL_VERSION) { + if (!modInfo->getNewestVersion().isValid()) { + return QVariant(); + } else if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) { + return QBrush(Qt::red); + } else { + return QBrush(Qt::darkGreen); + } + } + return QVariant(); + } else if ((role == Qt::BackgroundRole) + || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) { + bool overwrite = m_Overwrite.find(modIndex) != m_Overwrite.end(); + bool archiveOverwrite = m_ArchiveOverwrite.find(modIndex) != m_ArchiveOverwrite.end(); + bool archiveLooseOverwrite = m_ArchiveLooseOverwrite.find(modIndex) != m_ArchiveLooseOverwrite.end(); + bool overwritten = m_Overwritten.find(modIndex) != m_Overwritten.end(); + bool archiveOverwritten = m_ArchiveOverwritten.find(modIndex) != m_ArchiveOverwritten.end(); + bool archiveLooseOverwritten = m_ArchiveLooseOverwritten.find(modIndex) != m_ArchiveLooseOverwritten.end(); + if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_PLUGIN) { + return Settings::instance().modlistContainsPluginColor(); + } else if (overwritten || archiveLooseOverwritten) { + return Settings::instance().modlistOverwritingLooseColor(); + } else if (overwrite || archiveLooseOverwrite) { + return Settings::instance().modlistOverwrittenLooseColor(); + } else if (archiveOverwritten) { + return Settings::instance().modlistOverwritingArchiveColor(); + } else if (archiveOverwrite) { + return Settings::instance().modlistOverwrittenArchiveColor(); + } else if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) + && modInfo->getColor().isValid() + && ((role != ViewMarkingScrollBar::DEFAULT_ROLE) + || Settings::instance().colorSeparatorScrollbar())) { + return modInfo->getColor(); + } else { + return QVariant(); + } + } else if (role == Qt::ToolTipRole) { + if (column == COL_FLAGS) { + QString result; + + for (ModInfo::EFlag flag : modInfo->getFlags()) { + if (result.length() != 0) result += "
"; + result += getFlagText(flag, modInfo); + } + + return result; + } else if (column == COL_CONTENT) { + return contentsToToolTip(modInfo->getContents()); + } else if (column == COL_NAME) { + try { + return modInfo->getDescription(); + } catch (const std::exception &e) { + qCritical("invalid mod description: %s", e.what()); + return QString(); + } + } else if (column == COL_VERSION) { + QString text = tr("installed version: \"%1\", newest version: \"%2\"").arg(modInfo->getVersion().displayString(3)).arg(modInfo->getNewestVersion().displayString(3)); + if (modInfo->downgradeAvailable()) { + text += "
" + tr("The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn " + "(i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. " + "Either way you may want to \"upgrade\"."); + } + if (modInfo->getNexusID() > 0) { + if (!modInfo->canBeUpdated()) { + text += "
" + tr("This mod was last checked on %1. It will be available to check after %2.") + .arg(modInfo->getLastNexusUpdate().toLocalTime().toString(Qt::DefaultLocaleShortDate)) + .arg(modInfo->getLastNexusUpdate().toLocalTime().addSecs(3600).time().toString(Qt::DefaultLocaleShortDate)); + } else { + text += "
" + tr("This mod is eligible for an update check."); + } + } + return text; + } else if (column == COL_CATEGORY) { + const std::set &categories = modInfo->getCategories(); + std::wostringstream categoryString; + categoryString << ToWString(tr("Categories:
")); + CategoryFactory &categoryFactory = CategoryFactory::instance(); + for (std::set::const_iterator catIter = categories.begin(); + catIter != categories.end(); ++catIter) { + if (catIter != categories.begin()) { + categoryString << " , "; + } + try { + categoryString << "" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << ""; + } catch (const std::exception &e) { + qCritical("failed to generate tooltip: %s", e.what()); + return QString(); + } + } + + return ToQString(categoryString.str()); + } else if (column == COL_NOTES) { + return getFlagText(ModInfo::FLAG_NOTES, modInfo); + } else { + return QVariant(); + } + } else { + return QVariant(); + } +} + + +bool ModList::renameMod(int index, const QString &newName) +{ + QString nameFixed = newName; + if (!fixDirectoryName(nameFixed) || nameFixed.isEmpty()) { + MessageDialog::showMessage(tr("Invalid name"), nullptr); + return false; + } + + if (ModList::allMods().contains(nameFixed, Qt::CaseInsensitive) && nameFixed.toLower()!=ModInfo::getByIndex(index)->name().toLower() ) { + MessageDialog::showMessage(tr("Name is already in use by another mod"), nullptr); + return false; + } + + ModInfo::Ptr modInfo = ModInfo::getByIndex(index); + QString oldName = modInfo->name(); + if (nameFixed != oldName) { + // before we rename, ensure there is no scheduled asynchronous to rewrite + m_Profile->cancelModlistWrite(); + + + if (modInfo->setName(nameFixed)) + // Notice there is a good chance that setName() updated the modinfo indexes + // the modRenamed() call will refresh the indexes in the current profile + // and update the modlists in all profiles + emit modRenamed(oldName, nameFixed); + } + + // invalidate the currently displayed state of this list + notifyChange(-1); + return true; +} + +bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (m_Profile == nullptr) return false; + + if (static_cast(index.row()) >= ModInfo::getNumMods()) { + return false; + } + + int modID = index.row(); + + ModInfo::Ptr info = ModInfo::getByIndex(modID); + IModList::ModStates oldState = state(modID); + + bool result = false; + + emit aboutToChangeData(); + + if (role == Qt::CheckStateRole) { + bool enabled = value.toInt() == Qt::Checked; + if (m_Profile->modEnabled(modID) != enabled) { + m_Profile->setModEnabled(modID, enabled); + m_Modified = true; + m_LastCheck.restart(); + emit modlistChanged(index, role); + } + result = true; + emit dataChanged(index, index); + } else if (role == Qt::EditRole) { + switch (index.column()) { + case COL_NAME: { + auto flags = info->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) + { + result = renameMod(modID, value.toString() + "_separator"); + } + else + result = renameMod(modID, value.toString()); + } break; + case COL_PRIORITY: { + bool ok = false; + int newPriority = value.toInt(&ok); + if (ok && newPriority < 0) { + newPriority = 0; + } + if (ok) { + m_Profile->setModPriority(modID, newPriority); + + emit modorder_changed(); + result = true; + } else { + result = false; + } + } break; + case COL_MODID: { + bool ok = false; + int newID = value.toInt(&ok); + if (ok) { + info->setNexusID(newID); + emit modlistChanged(index, role); + result = true; + } else { + result = false; + } + } break; + case COL_VERSION: { + VersionInfo::VersionScheme scheme = info->getVersion().scheme(); + VersionInfo version(value.toString(), scheme, true); + if (version.isValid()) { + info->setVersion(version); + result = true; + } else { + result = false; + } + } break; + case COL_NOTES: { + info->setComments(value.toString()); + result = true; + } break; + default: { + qWarning("edit on column \"%s\" not supported", + getColumnName(index.column()).toUtf8().constData()); + result = false; + } break; + } + if (result) { + emit dataChanged(index, index); + } + } + + emit postDataChanged(); + + IModList::ModStates newState = state(modID); + if (oldState != newState) { + try { + m_ModStateChanged(info->name(), newState); + } catch (const std::exception &e) { + qCritical("failed to invoke state changed notification: %s", e.what()); + } catch (...) { + qCritical("failed to invoke state changed notification: unknown exception"); + } + } + + return result; +} + + +QVariant ModList::headerData(int section, Qt::Orientation orientation, + int role) const +{ + if (orientation == Qt::Horizontal) { + if (role == Qt::DisplayRole) { + return getColumnName(section); + } else if (role == Qt::ToolTipRole) { + return getColumnToolTip(section); + } else if (role == Qt::TextAlignmentRole) { + return QVariant(Qt::AlignCenter); + } else if (role == Qt::SizeHintRole) { + QSize temp = m_FontMetrics.size(Qt::TextSingleLine, getColumnName(section)); + temp.rwidth() += 20; + temp.rheight() += 12; + return temp; + } + } + return QAbstractItemModel::headerData(section, orientation, role); +} + + +Qt::ItemFlags ModList::flags(const QModelIndex &modelIndex) const +{ + Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex); + if (modelIndex.internalId() < 0) { + return Qt::ItemIsEnabled; + } + if (modelIndex.isValid()) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modelIndex.row()); + std::vector flags = modInfo->getFlags(); + if (modInfo->getFixedPriority() == INT_MIN) { + result |= Qt::ItemIsDragEnabled; + result |= Qt::ItemIsUserCheckable; + if ((modelIndex.column() == COL_PRIORITY) || + (modelIndex.column() == COL_VERSION) || + (modelIndex.column() == COL_MODID)) { + result |= Qt::ItemIsEditable; + } + if (((modelIndex.column() == COL_NAME) || + (modelIndex.column() == COL_NOTES)) + && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end())) { + result |= Qt::ItemIsEditable; + } + } + if (m_DropOnItems + && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end())) { + result |= Qt::ItemIsDropEnabled; + } + } else { + if (!m_DropOnItems) result |= Qt::ItemIsDropEnabled; + } + return result; +} + + +QStringList ModList::mimeTypes() const +{ + QStringList result = QAbstractItemModel::mimeTypes(); + result.append("text/uri-list"); + return result; +} + +QMimeData *ModList::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *result = QAbstractItemModel::mimeData(indexes); + result->setData("text/plain", "mod"); + return result; +} + +void ModList::changeModPriority(std::vector sourceIndices, int newPriority) +{ + if (m_Profile == nullptr) return; + + emit layoutAboutToBeChanged(); + Profile *profile = m_Profile; + + // sort the moving mods by ascending priorities + std::sort(sourceIndices.begin(), sourceIndices.end(), + [profile](const int &LHS, const int &RHS) { + return profile->getModPriority(LHS) > profile->getModPriority(RHS); + }); + + // move mods that are decreasing in priority + for (std::vector::const_iterator iter = sourceIndices.begin(); + iter != sourceIndices.end(); ++iter) { + int oldPriority = profile->getModPriority(*iter); + if (oldPriority > newPriority) { + profile->setModPriority(*iter, newPriority); + m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority); + } + } + + // sort the moving mods by descending priorities + std::sort(sourceIndices.begin(), sourceIndices.end(), + [profile](const int &LHS, const int &RHS) { + return profile->getModPriority(LHS) < profile->getModPriority(RHS); + }); + + // if at least one mod is increasing in priority, the target index is + // that of the row BELOW the dropped location, otherwise it's the one above + for (std::vector::const_iterator iter = sourceIndices.begin(); + iter != sourceIndices.end(); ++iter) { + int oldPriority = profile->getModPriority(*iter); + if (oldPriority < newPriority) { + --newPriority; + break; + } + } + + // move mods that are increasing in priority + for (std::vector::const_iterator iter = sourceIndices.begin(); + iter != sourceIndices.end(); ++iter) { + int oldPriority = profile->getModPriority(*iter); + if (oldPriority < newPriority) { + profile->setModPriority(*iter, newPriority); + m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority); + } + } + + emit layoutChanged(); + + emit modorder_changed(); +} + + +void ModList::changeModPriority(int sourceIndex, int newPriority) +{ + if (m_Profile == nullptr) return; + emit layoutAboutToBeChanged(); + + m_Profile->setModPriority(sourceIndex, newPriority); + + emit layoutChanged(); + + emit modorder_changed(); +} + +void ModList::setOverwriteMarkers(const std::set &overwrite, const std::set &overwritten) +{ + m_Overwrite = overwrite; + m_Overwritten = overwritten; + notifyChange(0, rowCount() - 1); +} + +void ModList::setArchiveOverwriteMarkers(const std::set &overwrite, const std::set &overwritten) +{ + m_ArchiveOverwrite = overwrite; + m_ArchiveOverwritten = overwritten; + notifyChange(0, rowCount() - 1); +} + +void ModList::setArchiveLooseOverwriteMarkers(const std::set &overwrite, const std::set &overwritten) +{ + m_ArchiveLooseOverwrite = overwrite; + m_ArchiveLooseOverwritten = overwritten; + notifyChange(0, rowCount() - 1); +} + +void ModList::setPluginContainer(PluginContainer *pluginContianer) +{ + m_PluginContainer = pluginContianer; +} + +bool ModList::modInfoAboutToChange(ModInfo::Ptr info) +{ + if (m_ChangeInfo.name.isEmpty()) { + m_ChangeInfo.name = info->name(); + m_ChangeInfo.state = state(info->name()); + return true; + } else { + return false; + } +} + +void ModList::modInfoChanged(ModInfo::Ptr info) +{ + if (info->name() == m_ChangeInfo.name) { + IModList::ModStates newState = state(info->name()); + if (m_ChangeInfo.state != newState) { + m_ModStateChanged(info->name(), newState); + } + + int row = ModInfo::getIndex(info->name()); + info->testValid(); + emit aboutToChangeData(); + emit dataChanged(index(row, 0), index(row, columnCount())); + emit postDataChanged(); + } else { + qCritical("modInfoChanged not called after modInfoAboutToChange"); + } + m_ChangeInfo.name = QString(); +} + +void ModList::disconnectSlots() { + m_ModMoved.disconnect_all_slots(); + m_ModStateChanged.disconnect_all_slots(); +} + +int ModList::timeElapsedSinceLastChecked() const +{ + return m_LastCheck.elapsed(); +} + +void ModList::highlightMods(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry) +{ + for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { + ModInfo::getByIndex(i)->setPluginSelected(false); + } + for (QModelIndex idx : selection->selectedRows(PluginList::COL_NAME)) { + QString modName = idx.data().toString(); + + const MOShared::FileEntry::Ptr fileEntry = directoryEntry.findFile(modName.toStdWString()); + if (fileEntry.get() != nullptr) { + bool archive = false; + std::vector>> origins; + { + std::vector>> alternatives = fileEntry->getAlternatives(); + origins.insert(origins.end(), std::pair>(fileEntry->getOrigin(archive), fileEntry->getArchive())); + } + for (auto originInfo : origins) { + MOShared::FilesOrigin &origin = directoryEntry.getOriginByID(originInfo.first); + for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { + if (ModInfo::getByIndex(i)->internalName() == QString::fromStdWString(origin.getName())) { + ModInfo::getByIndex(i)->setPluginSelected(true); + break; + } + } + } + } + } + notifyChange(0, rowCount() - 1); +} + +IModList::ModStates ModList::state(unsigned int modIndex) const +{ + IModList::ModStates result; + if (modIndex != UINT_MAX) { + result |= IModList::STATE_EXISTS; + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + if (modInfo->isEmpty()) { + result |= IModList::STATE_EMPTY; + } + if (modInfo->endorsedState() == ModInfo::ENDORSED_TRUE) { + result |= IModList::STATE_ENDORSED; + } + if (modInfo->isValid()) { + result |= IModList::STATE_VALID; + } + if (modInfo->isRegular()) { + QSharedPointer modInfoRegular = modInfo.staticCast(); + if (modInfoRegular->isAlternate() && !modInfoRegular->isConverted()) + result |= IModList::STATE_ALTERNATE; + if (!modInfo->isValid() && modInfoRegular->isValidated()) + result |= IModList::STATE_VALID; + } + if (modInfo->canBeEnabled()) { + if (m_Profile->modEnabled(modIndex)) { + result |= IModList::STATE_ACTIVE; + } + } else { + result |= IModList::STATE_ESSENTIAL; + } + } + return result; +} + +QString ModList::displayName(const QString &internalName) const +{ + unsigned int modIndex = ModInfo::getIndex(internalName); + if (modIndex == UINT_MAX) { + // might be better to throw an exception? + return internalName; + } else { + return ModInfo::getByIndex(modIndex)->name(); + } +} + +QStringList ModList::allMods() const +{ + QStringList result; + for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { + result.append(ModInfo::getByIndex(i)->internalName()); + } + return result; +} + +IModList::ModStates ModList::state(const QString &name) const +{ + unsigned int modIndex = ModInfo::getIndex(name); + + return state(modIndex); +} + +bool ModList::setActive(const QString &name, bool active) +{ + unsigned int modIndex = ModInfo::getIndex(name); + if (modIndex == UINT_MAX) { + return false; + } else { + m_Profile->setModEnabled(modIndex, active); + + IModList::ModStates newState = state(modIndex); + m_ModStateChanged(name, newState); + return true; + } +} + +int ModList::priority(const QString &name) const +{ + unsigned int modIndex = ModInfo::getIndex(name); + if (modIndex == UINT_MAX) { + return -1; + } else { + return m_Profile->getModPriority(modIndex); + } +} + +bool ModList::setPriority(const QString &name, int newPriority) +{ + if ((newPriority < 0) || (newPriority >= static_cast(m_Profile->numRegularMods()))) { + return false; + } + + unsigned int modIndex = ModInfo::getIndex(name); + if (modIndex == UINT_MAX) { + return false; + } else { + m_Profile->setModPriority(modIndex, newPriority); + notifyChange(modIndex); + return true; + } +} + +bool ModList::onModStateChanged(const std::function &func) +{ + auto conn = m_ModStateChanged.connect(func); + return conn.connected(); +} + +bool ModList::onModMoved(const std::function &func) +{ + auto conn = m_ModMoved.connect(func); + return conn.connected(); +} + +bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &parent) +{ + QStringList source; + QStringList target; + + if (row == -1) { + row = parent.row(); + } + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); + QDir modDirectory(modInfo->absolutePath()); + QDir gameDirectory(Settings::instance().getOverwriteDirectory()); + + unsigned int overwriteIndex = ModInfo::findMod([] (ModInfo::Ptr mod) -> bool { + std::vector flags = mod->getFlags(); + return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); }); + + QString overwriteName = ModInfo::getByIndex(overwriteIndex)->name(); + + for (const QUrl &url : mimeData->urls()) { + //qDebug("URL drop requested: %s", qUtf8Printable(url.toLocalFile())); + if (!url.isLocalFile()) { + qDebug("URL drop ignored: Not a local file."); + continue; + } + QString relativePath = gameDirectory.relativeFilePath(url.toLocalFile()); + if (relativePath.startsWith("..")) { + qDebug("URL drop ignored: relative path starts with .."); + continue; + } + source.append(url.toLocalFile()); + target.append(modDirectory.absoluteFilePath(relativePath)); + emit fileMoved(relativePath, overwriteName, modInfo->name()); + } + + if (source.count() != 0) { + if (!shellMove(source, target)) { + qDebug("Move failed %lu",::GetLastError()); + return false; + } + } + + if (!modInfo->isValid()) { + modInfo->testValid(); + } + + return true; +} + +bool ModList::dropMod(const QMimeData *mimeData, int row, const QModelIndex &parent) +{ + + try { + QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist"); + QDataStream stream(&encoded, QIODevice::ReadOnly); + std::vector sourceRows; + + while (!stream.atEnd()) { + int sourceRow, col; + QMap roleDataMap; + stream >> sourceRow >> col >> roleDataMap; + if (col == 0) { + sourceRows.push_back(sourceRow); + } + } + + if (row == -1) { + row = parent.row(); + } + + if ((row < 0) || (static_cast(row) >= ModInfo::getNumMods())) { + return false; + } + + int newPriority = 0; + { + if ((row < 0) || (row > static_cast(m_Profile->numRegularMods()))) { + newPriority = m_Profile->numRegularMods() + 1; + } else { + newPriority = m_Profile->getModPriority(row); + } + if (newPriority == -1) { + newPriority = m_Profile->numRegularMods() + 1; + } + } + changeModPriority(sourceRows, newPriority); + } catch (const std::exception &e) { + reportError(tr("drag&drop failed: %1").arg(e.what())); + } + + return false; +} + + +bool ModList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) { + return true; + } + + if (m_Profile == nullptr) return false; + + if (mimeData->hasUrls()) { + return dropURLs(mimeData, row, parent); + } else if (mimeData->hasText()) { + return dropMod(mimeData, row, parent); + } else { + return false; + } +} + +void ModList::removeRowForce(int row, const QModelIndex &parent) +{ + if (static_cast(row) >= ModInfo::getNumMods()) { + return; + } + if (m_Profile == nullptr) return; + + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); + + bool wasEnabled = m_Profile->modEnabled(row); + + m_Profile->setModEnabled(row, false); + + m_Profile->cancelModlistWrite(); + beginRemoveRows(parent, row, row); + ModInfo::removeMod(row); + endRemoveRows(); + m_Profile->refreshModStatus(); // removes the mod from the status list + m_Profile->writeModlist(); // this ensures the modified list gets written back before new mods can be installed + + if (wasEnabled) { + emit removeOrigin(modInfo->name()); + } + + emit modUninstalled(modInfo->getInstallationFile()); +} + +bool ModList::removeRows(int row, int count, const QModelIndex &parent) +{ + if (static_cast(row) >= ModInfo::getNumMods()) { + return false; + } + if (m_Profile == nullptr) { + return false; + } + + bool success = false; + + if (count == 1) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); + std::vector flags = modInfo->getFlags(); + if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) && (QDir(modInfo->absolutePath()).count() > 2)) { + emit clearOverwrite(); + success = true; + } + } + + for (int i = 0; i < count; ++i) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(row + i); + if (!modInfo->isRegular()) { + continue; + } + + success = true; + + QMessageBox confirmBox(QMessageBox::Question, tr("Confirm"), + tr("Are you sure you want to remove \"%1\"?").arg(modInfo->name()), + QMessageBox::Yes | QMessageBox::No); + + if (confirmBox.exec() == QMessageBox::Yes) { + m_Profile->setModEnabled(row + i, false); + removeRowForce(row + i, parent); + } + } + + return success; +} + + +void ModList::notifyChange(int rowStart, int rowEnd) +{ + if (rowStart < 0) { + m_Overwrite.clear(); + m_Overwritten.clear(); + m_ArchiveOverwrite.clear(); + m_ArchiveOverwritten.clear(); + m_ArchiveLooseOverwrite.clear(); + m_ArchiveLooseOverwritten.clear(); + beginResetModel(); + endResetModel(); + } else { + if (rowEnd == -1) { + rowEnd = rowStart; + } + emit dataChanged(this->index(rowStart, 0), this->index(rowEnd, this->columnCount() - 1)); + } +} + + +QModelIndex ModList::index(int row, int column, const QModelIndex&) const +{ + if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) { + return QModelIndex(); + } + QModelIndex res = createIndex(row, column, row); + return res; +} + + +QModelIndex ModList::parent(const QModelIndex&) const +{ + return QModelIndex(); +} + +QMap ModList::itemData(const QModelIndex &index) const +{ + QMap result = QAbstractItemModel::itemData(index); + result[Qt::UserRole] = data(index, Qt::UserRole); + return result; +} + + +void ModList::dropModeUpdate(bool dropOnItems) +{ + if (m_DropOnItems != dropOnItems) { + m_DropOnItems = dropOnItems; + } +} + + +QString ModList::getColumnName(int column) +{ + switch (column) { + case COL_FLAGS: return tr("Flags"); + case COL_CONTENT: return tr("Content"); + case COL_NAME: return tr("Mod Name"); + case COL_VERSION: return tr("Version"); + case COL_PRIORITY: return tr("Priority"); + case COL_CATEGORY: return tr("Category"); + case COL_GAME: return tr("Source Game"); + case COL_MODID: return tr("Nexus ID"); + case COL_INSTALLTIME: return tr("Installation"); + case COL_NOTES: return tr("Notes"); + default: return tr("unknown"); + } +} + + +QString ModList::getColumnToolTip(int column) +{ + switch (column) { + case COL_NAME: return tr("Name of your mods"); + case COL_VERSION: return tr("Version of the mod (if available)"); + case COL_PRIORITY: return tr("Installation priority of your mod. The higher, the more \"important\" it is and thus " + "overwrites files from mods with lower priority."); + case COL_CATEGORY: return tr("Category of the mod."); + case COL_GAME: return tr("The source game which was the origin of this mod."); + case COL_MODID: return tr("Id of the mod as used on Nexus."); + case COL_FLAGS: return tr("Emblemes to highlight things that might require attention."); + case COL_CONTENT: return tr("Depicts the content of the mod:
" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + + "
Game plugins (esp/esm/esl)
Interface
Meshes
BSA
Textures
Sounds
Music
Strings
Scripts (Papyrus)
Script Extender plugins
SkyProc Patcher
Mod Configuration Menu
INI files
ModGroup files
"); + case COL_INSTALLTIME: return tr("Time this mod was installed"); + case COL_NOTES: return tr("User notes about the mod"); + default: return tr("unknown"); + } +} + + +bool ModList::moveSelection(QAbstractItemView *itemView, int direction) +{ + QItemSelectionModel *selectionModel = itemView->selectionModel(); + + const QAbstractProxyModel *proxyModel = qobject_cast(selectionModel->model()); + const QSortFilterProxyModel *filterModel = nullptr; + + while ((filterModel == nullptr) && (proxyModel != nullptr)) { + filterModel = qobject_cast(proxyModel); + if (filterModel == nullptr) { + proxyModel = qobject_cast(proxyModel->sourceModel()); + } + } + if (filterModel == nullptr) { + return true; + } + + int offset = -1; + if (((direction < 0) && (filterModel->sortOrder() == Qt::DescendingOrder)) || + ((direction > 0) && (filterModel->sortOrder() == Qt::AscendingOrder))) { + offset = 1; + } + + QModelIndexList rows = selectionModel->selectedRows(); + if (direction > 0) { + for (int i = 0; i < rows.size() / 2; ++i) { + rows.swap(i, rows.size() - i - 1); + } + } + for (QModelIndex idx : rows) { + if (filterModel != nullptr) { + idx = filterModel->mapToSource(idx); + } + int newPriority = m_Profile->getModPriority(idx.row()) + offset; + if ((newPriority >= 0) && (newPriority < static_cast(m_Profile->numRegularMods()))) { + m_Profile->setModPriority(idx.row(), newPriority); + notifyChange(idx.row()); + } + } + emit modorder_changed(); + return true; +} + +bool ModList::deleteSelection(QAbstractItemView *itemView) +{ + QItemSelectionModel *selectionModel = itemView->selectionModel(); + + QModelIndexList rows = selectionModel->selectedRows(); + if (rows.count() > 1) { + emit removeSelectedMods(); + } else if (rows.count() == 1) { + removeRow(rows[0].data(Qt::UserRole + 1).toInt(), QModelIndex()); + } + return true; +} + +bool ModList::toggleSelection(QAbstractItemView *itemView) +{ + emit aboutToChangeData(); + + QItemSelectionModel *selectionModel = itemView->selectionModel(); + + QList modsToEnable; + QList modsToDisable; + QModelIndexList dirtyMods; + for (QModelIndex idx : selectionModel->selectedRows()) { + int modId = idx.data(Qt::UserRole + 1).toInt(); + if (m_Profile->modEnabled(modId)) { + modsToDisable.append(modId); + dirtyMods.append(idx); + } else { + modsToEnable.append(modId); + dirtyMods.append(idx); + } + } + + m_Profile->setModsEnabled(modsToEnable, modsToDisable); + + emit modlistChanged(dirtyMods, 0); + + m_Modified = true; + m_LastCheck.restart(); + + emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); + + emit postDataChanged(); + + return true; +} + +bool ModList::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::ContextMenu) { + QContextMenuEvent *contextEvent = static_cast(event); + QWidget *object = qobject_cast(obj); + if ((object != nullptr) && (contextEvent->reason() == QContextMenuEvent::Mouse)) { + emit requestColumnSelect(object->mapToGlobal(contextEvent->pos())); + + return true; + } + } else if ((event->type() == QEvent::KeyPress) && (m_Profile != nullptr)) { + QAbstractItemView *itemView = qobject_cast(obj); + QKeyEvent *keyEvent = static_cast(event); + + if ((itemView != nullptr) + && (keyEvent->modifiers() == Qt::ControlModifier) + && ((keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down))) { + return moveSelection(itemView, keyEvent->key() == Qt::Key_Up ? -1 : 1); + } else if (keyEvent->key() == Qt::Key_Delete) { + return deleteSelection(itemView); + } else if (keyEvent->key() == Qt::Key_Space) { + return toggleSelection(itemView); + } + return QAbstractItemModel::eventFilter(obj, event); + } + return QAbstractItemModel::eventFilter(obj, event); +} + +//note: caller needs to make sure sort proxy is updated +void ModList::enableSelected(const QItemSelectionModel *selectionModel) +{ + if (selectionModel->hasSelection()) { + QList modsToEnable; + for (auto row : selectionModel->selectedRows(COL_PRIORITY)) { + int modID = m_Profile->modIndexByPriority(row.data().toInt()); + modsToEnable.append(modID); + } + m_Profile->setModsEnabled(modsToEnable, QList()); + } +} + +//note: caller needs to make sure sort proxy is updated +void ModList::disableSelected(const QItemSelectionModel *selectionModel) +{ + if (selectionModel->hasSelection()) { + QList modsToDisable; + for (auto row : selectionModel->selectedRows(COL_PRIORITY)) { + int modID = m_Profile->modIndexByPriority(row.data().toInt()); + modsToDisable.append(modID); + } + m_Profile->setModsEnabled(QList(), modsToDisable); + } +} -- cgit v1.3.1