summaryrefslogtreecommitdiff
path: root/src/modlist.cpp
diff options
context:
space:
mode:
authorLostDragonist <lost.dragonist@gmail.com>2019-05-03 21:32:33 -0500
committerLostDragonist <lost.dragonist@gmail.com>2019-05-03 21:32:33 -0500
commit179a73857125ee604f42b0d5c2d765183c86d2c7 (patch)
treeb9b3f9d62bd5640de839d150a53ab8ef119dab9c /src/modlist.cpp
parente2b799bd6b5cfd51969fefd1dab5e5b1b7e5f81c (diff)
parent907c5468424b48774f5da2a6b5f96f26590987b0 (diff)
Merge pull request #695 from ModOrganizer2/Develop
Stage for Release 2.2.0
Diffstat (limited to 'src/modlist.cpp')
-rw-r--r--src/modlist.cpp2791
1 files changed, 1441 insertions, 1350 deletions
diff --git a/src/modlist.cpp b/src/modlist.cpp
index 16d2cd07..e2398cd6 100644
--- a/src/modlist.cpp
+++ b/src/modlist.cpp
@@ -1,1350 +1,1441 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#include "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 <appconfig.h>
-#include <utility.h>
-#include <report.h>
-
-#include <QFileInfo>
-#include <QDir>
-#include <QDirIterator>
-#include <QMimeData>
-#include <QStandardItemModel>
-#include <QMessageBox>
-#include <QStringList>
-#include <QEvent>
-#include <QContextMenuEvent>
-#include <QMenu>
-#include <QCheckBox>
-#include <QWidgetAction>
-#include <QAbstractItemView>
-#include <QSortFilterProxyModel>
-#include <QApplication>
-#include <QFontDatabase>
-
-#include <sstream>
-#include <stdexcept>
-#include <algorithm>
-
-
-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("<i>%1</i>").arg(modInfo->comments());
- if (!modInfo->notes().isEmpty())
- output << QString("<i>%1</i>").arg(modInfo->notes()).replace("\n", "<br>");
- return output.join("<br>");
- }
- case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites files");
- case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten files");
- case ModInfo::FLAG_CONFLICT_MIXED: return tr("Overwrites & Overwritten");
- case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant");
- case ModInfo::FLAG_ALTERNATE_GAME: return tr("Alternate game source");
- default: return "";
- }
-}
-
-
-QVariantList ModList::contentsToIcons(const std::vector<ModInfo::EContent> &contents) const
-{
- QVariantList result;
- std::set<ModInfo::EContent> 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<ModInfo::EContent> &contents) const
-{
- QString result("<table cellspacing=7>");
-
- std::set<ModInfo::EContent> 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("<tr><td><img src=\"%1\" width=32/></td>"
- "<td valign=\"middle\">%2</td></tr>")
- .arg(std::get<0>(iter->second)).arg(std::get<1>(iter->second)));
- }
- }
- result.append("</table>");
- 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<IPluginGame>()) {
- if (game->gameShortName() == modInfo->getGameName())
- 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<int> 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)) {
- if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_PLUGIN) {
- return Settings::instance().modlistContainsPluginColor();
- } else if (m_Overwrite.find(modIndex) != m_Overwrite.end()) {
- return Settings::instance().modlistOverwrittenLooseColor();
- }
- else if (m_Overwritten.find(modIndex) != m_Overwritten.end()) {
- return Settings::instance().modlistOverwritingLooseColor();
- } 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 += "<br>";
- 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()).arg(modInfo->getNewestVersion().displayString());
- if (modInfo->downgradeAvailable()) {
- text += "<br>" + 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<int> &categories = modInfo->getCategories();
- std::wostringstream categoryString;
- categoryString << ToWString(tr("Categories: <br>"));
- CategoryFactory &categoryFactory = CategoryFactory::instance();
- for (std::set<int>::const_iterator catIter = categories.begin();
- catIter != categories.end(); ++catIter) {
- if (catIter != categories.begin()) {
- categoryString << " , ";
- }
- try {
- categoryString << "<span style=\"white-space: nowrap;\"><i>" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << "</font></span>";
- } 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<unsigned int>(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 modlist_changed(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 modlist_changed(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<ModInfo::EFlag> 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<int> 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<int>::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<int>::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<int>::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<unsigned int> &overwrite, const std::set<unsigned int> &overwritten)
-{
- m_Overwrite = overwrite;
- m_Overwritten = 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<std::pair<int, std::wstring>> origins;
- {
- std::vector<std::pair<int, std::wstring>> alternatives = fileEntry->getAlternatives();
- origins.insert(origins.end(), std::pair<int, std::wstring>(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> modInfoRegular = modInfo.staticCast<ModInfoRegular>();
- 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<int>(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<void (const QString &, IModList::ModStates)> &func)
-{
- auto conn = m_ModStateChanged.connect(func);
- return conn.connected();
-}
-
-bool ModList::onModMoved(const std::function<void (const QString &, int, int)> &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<ModInfo::EFlag> 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()) {
- if (!url.isLocalFile()) {
- continue;
- }
- QString relativePath = gameDirectory.relativeFilePath(url.toLocalFile());
- if (relativePath.startsWith("..")) {
- qDebug("%s drop ignored", qPrintable(url.toLocalFile()));
- continue;
- }
- source.append(url.toLocalFile());
- target.append(modDirectory.absoluteFilePath(relativePath));
- emit fileMoved(relativePath, overwriteName, modInfo->name());
- }
-
- if (source.count() != 0) {
- shellMove(source, target);
- }
-
- 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<int> sourceRows;
-
- while (!stream.atEnd()) {
- int sourceRow, col;
- QMap<int, QVariant> roleDataMap;
- stream >> sourceRow >> col >> roleDataMap;
- if (col == 0) {
- sourceRows.push_back(sourceRow);
- }
- }
-
- if (row == -1) {
- row = parent.row();
- }
-
- if ((row < 0) || (static_cast<unsigned int>(row) >= ModInfo::getNumMods())) {
- return false;
- }
-
- int newPriority = 0;
- {
- if ((row < 0) || (row > static_cast<int>(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<unsigned int>(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<unsigned int>(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<ModInfo::EFlag> 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();
- 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<int, QVariant> ModList::itemData(const QModelIndex &index) const
-{
- QMap<int, QVariant> 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:<br>"
- "<table cellspacing=7>"
- "<tr><td><img src=\":/MO/gui/content/plugin\" width=32/></td><td>Game plugins (esp/esm/esl)</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/interface\" width=32/></td><td>Interface</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/mesh\" width=32/></td><td>Meshes</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/bsa\" width=32/></td><td>BSA</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/texture\" width=32/></td><td>Textures</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/sound\" width=32/></td><td>Sounds</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/music\" width=32/></td><td>Music</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/string\" width=32/></td><td>Strings</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/script\" width=32/></td><td>Scripts (Papyrus)</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/skse\" width=32/></td><td>Script Extender plugins</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/skyproc\" width=32/></td><td>SkyProc Patcher</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/menu\" width=32/></td><td>Mod Configuration Menu</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/inifile\" width=32/></td><td>INI files</td></tr>"
- "<tr><td><img src=\":/MO/gui/content/modgroup\" width=32/></td><td>ModGroup files</td></tr>"
-
- "</table>");
- 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<const QAbstractProxyModel*>(selectionModel->model());
- const QSortFilterProxyModel *filterModel = nullptr;
-
- while ((filterModel == nullptr) && (proxyModel != nullptr)) {
- filterModel = qobject_cast<const QSortFilterProxyModel*>(proxyModel);
- if (filterModel == nullptr) {
- proxyModel = qobject_cast<const QAbstractProxyModel*>(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<int>(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();
-
- for (QModelIndex idx : selectionModel->selectedRows()) {
- int modId = idx.data(Qt::UserRole + 1).toInt();
- m_Profile->setModEnabled(modId, !m_Profile->modEnabled(modId));
- emit modlist_changed(idx, 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<QContextMenuEvent*>(event);
- QWidget *object = qobject_cast<QWidget*>(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<QAbstractItemView*>(obj);
- QKeyEvent *keyEvent = static_cast<QKeyEvent*>(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);
-}
-
-
-void ModList::enableSelected(const QItemSelectionModel *selectionModel)
-{
- if (selectionModel->hasSelection()) {
- bool dirty = false;
- for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
- int modID = m_Profile->modIndexByPriority(row.data().toInt());
- if (!m_Profile->modEnabled(modID)) {
- m_Profile->setModEnabled(modID, true);
- emit modlist_changed(row, 0);
- }
- }
- }
-}
-
-
-void ModList::disableSelected(const QItemSelectionModel *selectionModel)
-{
- if (selectionModel->hasSelection()) {
- bool dirty = false;
- for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
- int modID = m_Profile->modIndexByPriority(row.data().toInt());
- if (m_Profile->modEnabled(modID)) {
- m_Profile->setModEnabled(modID, false);
- emit modlist_changed(row, 0);
- }
- }
- }
-}
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "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 <appconfig.h>
+#include <utility.h>
+#include <report.h>
+
+#include <QFileInfo>
+#include <QDir>
+#include <QDirIterator>
+#include <QMimeData>
+#include <QStandardItemModel>
+#include <QMessageBox>
+#include <QStringList>
+#include <QEvent>
+#include <QContextMenuEvent>
+#include <QMenu>
+#include <QCheckBox>
+#include <QWidgetAction>
+#include <QAbstractItemView>
+#include <QSortFilterProxyModel>
+#include <QApplication>
+#include <QFontDatabase>
+
+#include <sstream>
+#include <stdexcept>
+#include <algorithm>
+
+
+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("<i>%1</i>").arg(modInfo->comments());
+ if (!modInfo->notes().isEmpty())
+ output << QString("<i>%1</i>").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("<br>This mod is for a different game, "
+ "make sure it's compatible or it could cause crashes.");
+ case ModInfo::FLAG_TRACKED: return tr("Mod is being tracked on the website");
+ default: return "";
+ }
+}
+
+
+QVariantList ModList::contentsToIcons(const std::vector<ModInfo::EContent> &contents) const
+{
+ QVariantList result;
+ std::set<ModInfo::EContent> 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<ModInfo::EContent> &contents) const
+{
+ QString result("<table cellspacing=7>");
+
+ std::set<ModInfo::EContent> 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("<tr><td><img src=\"%1\" width=32/></td>"
+ "<td valign=\"middle\">%2</td></tr>")
+ .arg(std::get<0>(iter->second)).arg(std::get<1>(iter->second)));
+ }
+ }
+ result.append("</table>");
+ 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<IPluginGame>()) {
+ 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<int> 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 += "<br>";
+ 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 += "<br>" + 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->getNexusFileStatus() == 4) {
+ text += "<br>" + tr("This file has been marked as \"Old\". There is most likely an updated version of this file available.");
+ } else if (modInfo->getNexusFileStatus() == 6) {
+ text += "<br>" + tr("This file has been marked as \"Deleted\"! You may want to check for an update or remove the nexus ID from this mod!");
+ }
+ if (modInfo->getNexusID() > 0) {
+ if (!modInfo->canBeUpdated()) {
+ qint64 remains = QDateTime::currentDateTimeUtc().secsTo(modInfo->getExpires());
+ qint64 minutes = remains / 60;
+ qint64 seconds = remains % 60;
+ QString remainsStr(tr("%1 minute(s) and %2 second(s)").arg(minutes).arg(seconds));
+ text += "<br>" + tr("This mod will be available to check in %2.")
+ .arg(remainsStr);
+ }
+ }
+ return text;
+ } else if (column == COL_CATEGORY) {
+ const std::set<int> &categories = modInfo->getCategories();
+ std::wostringstream categoryString;
+ categoryString << ToWString(tr("Categories: <br>"));
+ CategoryFactory &categoryFactory = CategoryFactory::instance();
+ for (std::set<int>::const_iterator catIter = categories.begin();
+ catIter != categories.end(); ++catIter) {
+ if (catIter != categories.begin()) {
+ categoryString << " , ";
+ }
+ try {
+ categoryString << "<span style=\"white-space: nowrap;\"><i>" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << "</font></span>";
+ } 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<unsigned int>(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<ModInfo::EFlag> 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<int> 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<int>::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<int>::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<int>::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<unsigned int> &overwrite, const std::set<unsigned int> &overwritten)
+{
+ m_Overwrite = overwrite;
+ m_Overwritten = overwritten;
+ notifyChange(0, rowCount() - 1);
+}
+
+void ModList::setArchiveOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten)
+{
+ m_ArchiveOverwrite = overwrite;
+ m_ArchiveOverwritten = overwritten;
+ notifyChange(0, rowCount() - 1);
+}
+
+void ModList::setArchiveLooseOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &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<std::pair<int, std::pair<std::wstring, int>>> origins;
+ {
+ std::vector<std::pair<int, std::pair<std::wstring, int>>> alternatives = fileEntry->getAlternatives();
+ origins.insert(origins.end(), std::pair<int, std::pair<std::wstring, int>>(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> modInfoRegular = modInfo.staticCast<ModInfoRegular>();
+ 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<int>(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<void (const QString &, IModList::ModStates)> &func)
+{
+ auto conn = m_ModStateChanged.connect(func);
+ return conn.connected();
+}
+
+bool ModList::onModMoved(const std::function<void (const QString &, int, int)> &func)
+{
+ auto conn = m_ModMoved.connect(func);
+ return conn.connected();
+}
+
+bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &parent)
+{
+ if (row == -1) {
+ row = parent.row();
+ }
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(row);
+ QDir modDir = QDir(modInfo->absolutePath());
+
+ QDir allModsDir(Settings::instance().getModDirectory());
+ QDir overwriteDir(Settings::instance().getOverwriteDirectory());
+
+ QStringList sourceList;
+ QStringList targetList;
+ QList<QPair<QString,QString>> relativePathList;
+
+ unsigned int overwriteIndex = ModInfo::findMod([] (ModInfo::Ptr mod) -> bool {
+ std::vector<ModInfo::EFlag> flags = mod->getFlags();
+ return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); });
+
+ QString overwriteName = ModInfo::getByIndex(overwriteIndex)->name();
+
+ for (auto url : mimeData->urls()) {
+ //qDebug("URL drop requested: %s -> %s", qUtf8Printable(url.url()), qUtf8Printable(modDir.canonicalPath()));
+ if (!url.isLocalFile()) {
+ qDebug("URL drop ignored: \"%s\" is not a local file", qUtf8Printable(url.url()));
+ continue;
+ }
+
+ QFileInfo sourceInfo(url.toLocalFile());
+ QString sourceFile = sourceInfo.canonicalFilePath();
+
+ QString relativePath;
+ QString originName;
+
+ if (sourceFile.startsWith(allModsDir.canonicalPath())) {
+ QDir relativeDir(allModsDir.relativeFilePath(sourceFile));
+ QStringList splitPath = relativeDir.path().split("/");
+ originName = splitPath[0];
+ splitPath.pop_front();
+ relativePath = splitPath.join("/");
+ } else if (sourceFile.startsWith(overwriteDir.canonicalPath())) {
+ originName = overwriteName;
+ relativePath = overwriteDir.relativeFilePath(sourceFile);
+ } else {
+ qDebug("URL drop ignored: \"%s\" is not a known file to MO", qUtf8Printable(sourceFile));
+ continue;
+ }
+
+ QFileInfo targetInfo(modDir.absoluteFilePath(sourceInfo.fileName()));
+ sourceList << sourceFile;
+ targetList << targetInfo.absoluteFilePath();
+ relativePathList << QPair<QString,QString>(relativePath, originName);
+ }
+
+ if (sourceList.count()) {
+ if (!shellMove(sourceList, targetList)) {
+ qDebug("Failed to move file (error %d)", ::GetLastError());
+ return false;
+ }
+ }
+
+ for (auto iter : relativePathList) {
+ emit fileMoved(iter.first, iter.second, modInfo->name());
+ }
+
+ 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<int> sourceRows;
+
+ while (!stream.atEnd()) {
+ int sourceRow, col;
+ QMap<int, QVariant> roleDataMap;
+ stream >> sourceRow >> col >> roleDataMap;
+ if (col == 0) {
+ sourceRows.push_back(sourceRow);
+ }
+ }
+
+ if (row == -1) {
+ row = parent.row();
+ }
+
+ if ((row < 0) || (static_cast<unsigned int>(row) >= ModInfo::getNumMods())) {
+ return false;
+ }
+
+ int newPriority = 0;
+ {
+ if ((row < 0) || (row > static_cast<int>(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<unsigned int>(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<unsigned int>(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<ModInfo::EFlag> 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<int, QVariant> ModList::itemData(const QModelIndex &index) const
+{
+ QMap<int, QVariant> 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:<br>"
+ "<table cellspacing=7>"
+ "<tr><td><img src=\":/MO/gui/content/plugin\" width=32/></td><td>Game plugins (esp/esm/esl)</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/interface\" width=32/></td><td>Interface</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/mesh\" width=32/></td><td>Meshes</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/bsa\" width=32/></td><td>BSA</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/texture\" width=32/></td><td>Textures</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/sound\" width=32/></td><td>Sounds</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/music\" width=32/></td><td>Music</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/string\" width=32/></td><td>Strings</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/script\" width=32/></td><td>Scripts (Papyrus)</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/skse\" width=32/></td><td>Script Extender plugins</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/skyproc\" width=32/></td><td>SkyProc Patcher</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/menu\" width=32/></td><td>Mod Configuration Menu</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/inifile\" width=32/></td><td>INI files</td></tr>"
+ "<tr><td><img src=\":/MO/gui/content/modgroup\" width=32/></td><td>ModGroup files</td></tr>"
+
+ "</table>");
+ 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<const QAbstractProxyModel*>(selectionModel->model());
+ const QSortFilterProxyModel *filterModel = nullptr;
+
+ while ((filterModel == nullptr) && (proxyModel != nullptr)) {
+ filterModel = qobject_cast<const QSortFilterProxyModel*>(proxyModel);
+ if (filterModel == nullptr) {
+ proxyModel = qobject_cast<const QAbstractProxyModel*>(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<int>(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<unsigned int> modsToEnable;
+ QList<unsigned int> 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<QContextMenuEvent*>(event);
+ QWidget *object = qobject_cast<QWidget*>(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<QAbstractItemView*>(obj);
+ QKeyEvent *keyEvent = static_cast<QKeyEvent*>(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<unsigned int> modsToEnable;
+ for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
+ int modID = m_Profile->modIndexByPriority(row.data().toInt());
+ modsToEnable.append(modID);
+ }
+ m_Profile->setModsEnabled(modsToEnable, QList<unsigned int>());
+ }
+}
+
+//note: caller needs to make sure sort proxy is updated
+void ModList::disableSelected(const QItemSelectionModel *selectionModel)
+{
+ if (selectionModel->hasSelection()) {
+ QList<unsigned int> modsToDisable;
+ for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
+ int modID = m_Profile->modIndexByPriority(row.data().toInt());
+ modsToDisable.append(modID);
+ }
+ m_Profile->setModsEnabled(QList<unsigned int>(), modsToDisable);
+ }
+}