/*
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 "modinforegular.h"
#include "modlistdropinfo.h"
#include "modlistsortproxy.h"
#include "organizercore.h"
#include "pluginlist.h"
#include "qtgroupingproxy.h"
#include "settings.h"
#include "shared/directoryentry.h"
#include "shared/fileentry.h"
#include "shared/filesorigin.h"
#include "viewmarkingscrollbar.h"
#include "widgetutility.h"
#include "filesystemutilities.h"
#include "shared/appconfig.h"
#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, OrganizerCore* organizer)
: QAbstractItemModel(organizer), m_Organizer(organizer), m_Profile(nullptr),
m_NexusInterface(nullptr), m_Modified(false), m_InNotifyChange(false),
m_FontMetrics(QFont()), m_PluginContainer(pluginContainer)
{
m_LastCheck.start();
}
ModList::~ModList()
{
m_ModInstalled.disconnect_all_slots();
m_ModRemoved.disconnect_all_slots();
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;
}
QString ModList::getDisplayName(ModInfo::Ptr info) const
{
QString name = info->name();
if (info->isSeparator()) {
name = name.replace("_separator", "");
}
return name;
}
QString ModList::makeInternalName(ModInfo::Ptr info, QString name) const
{
if (info->isSeparator()) {
name += "_separator";
}
return name;
}
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_ALTERNATE_GAME:
return tr("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");
case ModInfo::FLAG_HIDDEN_FILES:
return tr("Contains hidden files");
default:
return "";
}
}
QString ModList::getConflictFlagText(ModInfo::EConflictFlag flag,
ModInfo::Ptr modInfo) const
{
switch (flag) {
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");
default:
return "";
}
}
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) ||
(column == COL_CONFLICTFLAGS)) {
return QVariant();
} else if (column == COL_NAME) {
return getDisplayName(modInfo);
} else if (column == COL_VERSION) {
VersionInfo verInfo = modInfo->version();
QString version = verInfo.displayString();
if (role != Qt::EditRole) {
if (version.isEmpty() && modInfo->canBeUpdated()) {
version = "?";
}
}
return version;
} else if (column == COL_PRIORITY) {
if (modInfo->hasAutomaticPriority()) {
return QVariant(); // hide priority for mods where it's fixed
} else {
return QString::number(m_Profile->getModPriority(modIndex));
}
} else if (column == COL_MODID) {
int modID = modInfo->nexusId();
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->gameName(), Qt::CaseInsensitive) ==
0)
return game->gameName();
}
}
return modInfo->gameName();
} else if (column == COL_CATEGORY) {
if (modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) {
return tr("Non-MO");
} else {
int category = modInfo->primaryCategory();
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) {
log::error("failed to retrieve category name: {}", e.what());
return QString();
}
} else {
log::warn("category {} doesn't exist (may have been removed)", category);
modInfo->setCategory(category, false);
return QString();
}
} else {
return QVariant();
}
}
} else if (column == COL_AUTHOR) {
return modInfo->author();
} else if (column == COL_UPLOADER) {
return modInfo->uploader();
} 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) {
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 == GroupingRole) {
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 {
return modInfo->nexusId();
}
} else if (role == IndexRole) {
return modIndex;
} else if (role == AggrRole) {
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 == GameNameRole) {
return modInfo->gameName();
} else if (role == PriorityRole) {
return m_Profile->getModPriority(modIndex);
} else if (role == Qt::FontRole) {
QFont result;
if (column == COL_NAME) {
if (modInfo->isSeparator()) {
result.setItalic(true);
result.setBold(true);
} else if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_INVALID) {
result.setItalic(true);
}
} else if (column == COL_CATEGORY && modInfo->isForeign()) {
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->version().scheme() == VersionInfo::SCHEME_DATE) {
return QIcon(":/MO/gui/version_date");
}
}
return QVariant();
} else if (role == Qt::ForegroundRole) {
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->newestVersion().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 == ScrollMarkRole) {
if (column == COL_NOTES && modInfo->color().isValid()) {
return modInfo->color();
} else if (modInfo->isSeparator() && modInfo->color().isValid() &&
(role != ScrollMarkRole ||
Settings::instance().colors().colorSeparatorScrollbar())) {
return modInfo->color();
} 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_CONFLICTFLAGS) {
QString result;
for (ModInfo::EConflictFlag flag : modInfo->getConflictFlags()) {
if (result.length() != 0)
result += "
";
result += getConflictFlagText(flag, modInfo);
}
return result;
} else if (column == COL_NAME) {
try {
return modInfo->getDescription();
} catch (const std::exception& e) {
log::error("invalid mod description: {}", e.what());
return QString();
}
} else if (column == COL_VERSION) {
QString text = tr("installed version: \"%1\", newest version: \"%2\"")
.arg(modInfo->version().displayString(3))
.arg(modInfo->newestVersion().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->getNexusFileStatus() == NexusInterface::FileStatus::OLD_VERSION) {
text += "
" + tr("This file has been marked as \"Old\". There is most "
"likely an updated version of this file available.");
} else if (modInfo->getNexusFileStatus() == NexusInterface::FileStatus::REMOVED ||
modInfo->getNexusFileStatus() ==
NexusInterface::FileStatus::ARCHIVED ||
modInfo->getNexusFileStatus() ==
NexusInterface::FileStatus::ARCHIVED_HIDDEN) {
text +=
"
" + 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->nexusId() > 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 +=
"
" + tr("This mod will be available to check in %2.").arg(remainsStr);
}
}
return text;
} else if (column == COL_CATEGORY) {
const std::set& categories = modInfo->getCategories();
if (categories.empty()) {
return QString();
}
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) {
log::error("failed to generate tooltip: {}", 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 tutorialModlistUpdate();
}
result = true;
emit dataChanged(index, index);
} else if (role == Qt::EditRole) {
switch (index.column()) {
case COL_NAME: {
result = renameMod(modID, makeInternalName(info, value.toString()));
} break;
case COL_PRIORITY: {
bool ok = false;
int newPriority = value.toInt(&ok);
if (ok) {
changeModPriority(modID, newPriority);
result = true;
} else {
result = false;
}
} break;
case COL_MODID: {
bool ok = false;
int newID = value.toInt(&ok);
if (ok) {
info->setNexusID(newID);
emit tutorialModlistUpdate();
result = true;
} else {
result = false;
}
} break;
case COL_VERSION: {
VersionInfo::VersionScheme scheme = info->version().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: {
log::warn("edit on column \"{}\" not supported",
getColumnName(index.column()).toUtf8().constData());
result = false;
} break;
}
if (result) {
emit dataChanged(index, index);
}
}
emit postDataChanged();
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 == MOBase::EnabledColumnRole) {
if (section == COL_CONTENT) {
return !m_Organizer->modDataContents().empty();
} else {
return true;
}
}
}
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());
if (!modInfo->hasAutomaticPriority()) {
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) &&
!modInfo->isForeign()) {
result |= Qt::ItemIsEditable;
}
}
if (modInfo->isSeparator() || m_DropOnMod) {
result |= Qt::ItemIsDropEnabled;
}
} else if (!m_DropOnMod) {
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", ModListDropInfo::ModText);
return result;
}
void ModList::changeModPriority(std::vector sourceIndices, int newPriority)
{
if (m_Profile == nullptr)
return;
emit layoutAboutToBeChanged();
// sort the moving mods by ascending priorities
std::sort(sourceIndices.begin(), sourceIndices.end(),
[=](const int& LHS, const int& RHS) {
return m_Profile->getModPriority(LHS) > m_Profile->getModPriority(RHS);
});
// move mods that are decreasing in priority
for (const auto& index : sourceIndices) {
int oldPriority = m_Profile->getModPriority(index);
if (oldPriority > newPriority) {
if (m_Profile->setModPriority(index, newPriority)) {
m_ModMoved(ModInfo::getByIndex(index)->name(), oldPriority, newPriority);
}
}
}
// sort the moving mods by descending priorities
std::sort(sourceIndices.begin(), sourceIndices.end(),
[=](const int& LHS, const int& RHS) {
return m_Profile->getModPriority(LHS) < m_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 (const auto& index : sourceIndices) {
int oldPriority = m_Profile->getModPriority(index);
if (oldPriority < newPriority) {
--newPriority;
break;
}
}
// move mods that are increasing in priority
for (const auto& index : sourceIndices) {
int oldPriority = m_Profile->getModPriority(index);
if (oldPriority < newPriority) {
if (m_Profile->setModPriority(index, newPriority)) {
m_ModMoved(ModInfo::getByIndex(index)->name(), oldPriority, newPriority);
}
}
}
emit layoutChanged();
QModelIndexList indices;
for (auto& idx : sourceIndices) {
indices.append(index(idx, 0, QModelIndex()));
}
emit modPrioritiesChanged(indices);
}
void ModList::changeModPriority(int sourceIndex, int newPriority)
{
if (m_Profile == nullptr)
return;
emit layoutAboutToBeChanged();
m_Profile->setModPriority(sourceIndex, newPriority);
emit layoutChanged();
emit modPrioritiesChanged({index(sourceIndex, 0)});
}
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->diskContentModified();
emit aboutToChangeData();
emit dataChanged(index(row, 0), index(row, columnCount()));
emit postDataChanged();
} else {
log::error("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();
}
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() == EndorsedState::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;
}
QStringList ModList::allModsByProfilePriority(MOBase::IProfile* profile) const
{
Profile* mo2Profile = profile == nullptr ? m_Organizer->currentProfile().get()
: static_cast(profile);
QStringList res;
for (auto& [priority, index] : mo2Profile->getAllIndexesByPriority()) {
auto modInfo = ModInfo::getByIndex(index);
if (!modInfo->isBackup() && !modInfo->isOverwrite()) {
res.push_back(modInfo->internalName());
}
}
return res;
}
MOBase::IModInterface* ModList::getMod(const QString& name) const
{
unsigned int index = ModInfo::getIndex(name);
return index == UINT_MAX ? nullptr : ModInfo::getByIndex(index).data();
}
bool ModList::removeMod(MOBase::IModInterface* mod)
{
bool result = ModInfo::removeMod(ModInfo::getIndex(mod->name()));
if (result) {
notifyModRemoved(mod->name());
}
return result;
}
MOBase::IModInterface* ModList::renameMod(MOBase::IModInterface* mod,
const QString& name)
{
unsigned int index = ModInfo::getIndex(mod->name());
if (index == UINT_MAX) {
if (auto* p = dynamic_cast(mod)) {
p->setName(name);
return p;
} else {
return nullptr;
}
} else {
if (renameMod(index, name)) {
return ModInfo::getByName(name).get();
} else {
return nullptr;
}
}
}
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) {
log::debug("Trying to {} mod {} which does not exist.",
active ? "enable" : "disable", name);
return false;
} else {
m_Profile->setModEnabled(modIndex, active);
return true;
}
}
int ModList::setActive(const QStringList& names, bool active)
{
// We only add indices for mods that exist (modIndex != UINT_MAX)
// and that can be enabled / disabled.
QList indices;
for (const auto& name : names) {
auto modIndex = ModInfo::getIndex(name);
if (modIndex != UINT_MAX) {
indices.append(modIndex);
} else {
log::debug("Trying to {} mod {} which does not exist.",
active ? "enable" : "disable", name);
}
}
if (active) {
m_Profile->setModsEnabled(indices, {});
} else {
m_Profile->setModsEnabled({}, indices);
}
return indices.size();
}
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)
{
unsigned int index = ModInfo::getIndex(name);
if (index == UINT_MAX) {
return false;
} else {
if (m_Profile->setModPriority(index, newPriority)) {
notifyChange(index);
}
return true;
}
}
boost::signals2::connection
ModList::onModInstalled(const std::function& func)
{
return m_ModInstalled.connect(func);
}
boost::signals2::connection
ModList::onModRemoved(const std::function& func)
{
return m_ModRemoved.connect(func);
}
boost::signals2::connection ModList::onModStateChanged(
const std::function&)>& func)
{
return m_ModStateChanged.connect(func);
}
void ModList::notifyModInstalled(MOBase::IModInterface* mod) const
{
m_ModInstalled(mod);
}
void ModList::notifyModRemoved(QString const& modName) const
{
m_ModRemoved(modName);
}
void ModList::notifyModStateChanged(QList modIndices) const
{
QModelIndexList indices;
std::map mods;
for (auto modIndex : modIndices) {
indices.append(index(modIndex, 0));
ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
mods.emplace(modInfo->name(), state(modIndex));
}
emit modStatesChanged(indices);
m_ModStateChanged(mods);
}
boost::signals2::connection
ModList::onModMoved(const std::function& func)
{
return m_ModMoved.connect(func);
}
int ModList::dropPriority(int row, const QModelIndex& parent) const
{
if (row == -1) {
row = parent.row();
}
if ((row < 0) || (static_cast(row) >= ModInfo::getNumMods())) {
return -1;
}
int newPriority = 0;
{
if (row < 0 || row >= rowCount()) {
newPriority = Profile::MaximumPriority;
} else {
newPriority = m_Profile->getModPriority(row);
}
}
return newPriority;
}
bool ModList::dropLocalFiles(const ModListDropInfo& dropInfo, int row,
const QModelIndex& parent)
{
if (row == -1) {
row = parent.row();
}
ModInfo::Ptr modInfo = ModInfo::getByIndex(row);
QDir modDir = QDir(modInfo->absolutePath());
QStringList sourceList;
QStringList targetList;
QList> relativePathList;
for (auto localUrl : dropInfo.localUrls()) {
QFileInfo sourceInfo(localUrl.url.toLocalFile());
if (localUrl.originName.compare("overwrite", Qt::CaseInsensitive) == 0) {
bool needsMove = true;
if (sourceInfo.isDir()) {
for (auto dir : m_Organizer->managedGame()->getModMappings().keys()) {
QDir overDir(m_Organizer->overwritePath());
if (sourceInfo.canonicalFilePath().compare(overDir.absoluteFilePath(dir),
Qt::CaseInsensitive) == 0) {
needsMove = false;
QDirIterator dirIter(overDir.absoluteFilePath(dir),
QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot);
while (dirIter.hasNext()) {
auto entry = dirIter.nextFileInfo();
QString sourceFile = entry.canonicalFilePath();
QFileInfo targetInfo(modDir.absoluteFilePath(
overDir.relativeFilePath(entry.absoluteFilePath())));
sourceList << sourceFile;
targetList << targetInfo.absoluteFilePath();
relativePathList << QPair(localUrl.relativePath,
localUrl.originName);
}
}
}
}
if (needsMove) {
QString sourceFile = sourceInfo.canonicalFilePath();
QFileInfo targetInfo(modDir.absoluteFilePath(localUrl.relativePath));
sourceList << sourceFile;
targetList << targetInfo.absoluteFilePath();
relativePathList << QPair(localUrl.relativePath,
localUrl.originName);
}
} else {
QString sourceFile = sourceInfo.canonicalFilePath();
QFileInfo targetInfo(modDir.absoluteFilePath(localUrl.relativePath));
sourceList << sourceFile;
targetList << targetInfo.absoluteFilePath();
relativePathList << QPair(localUrl.relativePath,
localUrl.originName);
}
}
if (sourceList.count()) {
if (!shellMove(sourceList, targetList)) {
log::debug("Failed to move file (error {})", ::GetLastError());
return false;
}
}
for (auto iter : relativePathList) {
emit fileMoved(iter.first, iter.second, modInfo->name());
}
if (!modInfo->isValid()) {
modInfo->diskContentModified();
}
return true;
}
void ModList::onDragEnter(const QMimeData* mimeData)
{
m_DropOnMod = ModListDropInfo(mimeData, *m_Organizer).isLocalFileDrop();
}
bool ModList::canDropMimeData(const QMimeData* mimeData, Qt::DropAction action, int row,
int column, const QModelIndex& parent) const
{
if (action == Qt::IgnoreAction) {
return false;
}
ModListDropInfo dropInfo(mimeData, *m_Organizer);
if (dropInfo.isLocalFileDrop()) {
if (row == -1 && parent.isValid()) {
ModInfo::Ptr modInfo = ModInfo::getByIndex(parent.row());
return modInfo->isRegular() && !modInfo->isSeparator();
}
} else if (dropInfo.isValid()) {
// drop on item
if (row == -1 && parent.isValid()) {
return true;
} else if (hasIndex(row, column, parent)) {
ModInfo::Ptr modInfo = ModInfo::getByIndex(row);
return !modInfo->isBackup() && (modInfo->isSeparator() || !parent.isValid());
} else {
return true;
}
}
return false;
}
bool ModList::dropMimeData(const QMimeData* mimeData, Qt::DropAction action, int row,
int, const QModelIndex& parent)
{
if (action == Qt::IgnoreAction) {
return true;
}
ModListDropInfo dropInfo(mimeData, *m_Organizer);
if (!m_Profile || !dropInfo.isValid()) {
return false;
}
int dropPriority = this->dropPriority(row, parent);
if (dropPriority == -1) {
return false;
}
if (dropInfo.isLocalFileDrop()) {
return dropLocalFiles(dropInfo, row, parent);
} else {
if (dropInfo.isModDrop()) {
changeModPriority(dropInfo.rows(), dropPriority);
} else if (dropInfo.isDownloadDrop()) {
emit downloadArchiveDropped(dropInfo.download(), dropPriority);
} else if (dropInfo.isExternalArchiveDrop()) {
emit externalArchiveDropped(dropInfo.externalUrl(), dropPriority);
} else if (dropInfo.isExternalFolderDrop()) {
emit externalFolderDropped(dropInfo.externalUrl(), dropPriority);
} else {
return false;
}
}
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);
m_Profile->refreshModStatus(); // removes the mod from the status list
endRemoveRows();
m_Profile->writeModlist(); // this ensures the modified list gets written back before
// new mods can be installed
notifyModRemoved(modInfo->name());
if (wasEnabled) {
emit removeOrigin(modInfo->name());
}
if (!modInfo->isBackup()) {
emit modUninstalled(modInfo->installationFile());
}
}
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);
if (modInfo->isOverwrite() && 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(getDisplayName(modInfo)),
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)
{
// this function can emit dataChanged(), which can eventually recurse back
// here; for example:
//
// - a filter is active in the mod list, such as "no categories"
// - mods are selected and a category is set on them
// - these mods get updated here and disappear from the list because they're
// not in "no categories" anymore
// - dataChanged() is emitted
// - it's picked up in MainWindow::modlistSelectionsChanged() because the
// selected mods are gone
// - it calls setOverwriteMarkers(), which calls notifyChange() again and
// ends up here
// - dataChanged() is emitted again
//
// at this point, MO crashes because dataChanged() is not reentrant: it's in
// the middle of modifying internal data and crashes when trying to change an
// internal vector
//
// long story short, this prevents reentrancy
if (m_InNotifyChange) {
return;
}
m_InNotifyChange = true;
Guard g([&] {
m_InNotifyChange = false;
});
if (rowStart < 0) {
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);
for (int role = Qt::UserRole; role < ModUserRole; ++role) {
result[role] = data(index, role);
}
return result;
}
QString ModList::getColumnName(int column)
{
switch (column) {
case COL_CONFLICTFLAGS:
return tr("Conflicts");
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_AUTHOR:
return tr("Author");
case COL_UPLOADER:
return tr("Uploader");
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) const
{
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("Primary category of the mod.");
case COL_AUTHOR:
return tr("Author(s) of the mod.");
case COL_UPLOADER:
return tr("Uploader of the mod. This is not necessarily the same as the author.");
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_CONFLICTFLAGS:
return tr("Indicators of file conflicts between mods.");
case COL_FLAGS:
return tr("Emblems to highlight things that might require attention.");
case COL_CONTENT: {
auto& contents = m_Organizer->modDataContents();
if (m_Organizer->modDataContents().empty()) {
return QString();
}
QString result =
tr("Depicts the content of the mod:") + "
" + "";
m_Organizer->modDataContents().forEachContent([&result](auto const& content) {
result += QString(" | %2 |
")
.arg(content.icon())
.arg(content.name());
});
return result + "
";
};
case COL_INSTALLTIME:
return tr("Time this mod was installed");
case COL_NOTES:
return tr("User notes about the mod");
default:
return tr("unknown");
}
}
void ModList::shiftModsPriority(const QModelIndexList& indices, int offset)
{
// retrieve the mod index and sort them by priority to avoid issue
// when moving them
std::vector allIndex;
for (auto& idx : indices) {
auto index = idx.data(IndexRole).toInt();
allIndex.push_back(index);
}
std::sort(allIndex.begin(), allIndex.end(), [=](int lhs, int rhs) {
bool cmp = m_Profile->getModPriority(lhs) < m_Profile->getModPriority(rhs);
return offset > 0 ? !cmp : cmp;
});
emit layoutAboutToBeChanged();
std::vector notify;
for (auto index : allIndex) {
int newPriority = m_Profile->getModPriority(index) + offset;
if (m_Profile->setModPriority(index, newPriority)) {
notify.push_back(index);
}
}
emit layoutChanged();
for (auto index : notify) {
notifyChange(index);
}
emit modPrioritiesChanged(indices);
}
void ModList::changeModsPriority(const QModelIndexList& indices, int priority)
{
if (indices.isEmpty()) {
return;
}
std::vector allIndex;
for (auto& idx : indices) {
auto index = idx.data(IndexRole).toInt();
allIndex.push_back(index);
}
if (allIndex.size() == 1) {
changeModPriority(allIndex[0], priority);
} else {
changeModPriority(allIndex, priority);
}
}
bool ModList::toggleState(const QModelIndexList& indices)
{
emit aboutToChangeData();
QList modsToEnable;
QList modsToDisable;
for (auto index : indices) {
auto idx = index.data(IndexRole).toInt();
if (m_Profile->modEnabled(idx)) {
modsToDisable.append(idx);
} else {
modsToEnable.append(idx);
}
}
m_Profile->setModsEnabled(modsToEnable, modsToDisable);
emit tutorialModlistUpdate();
m_Modified = true;
m_LastCheck.restart();
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
emit postDataChanged();
return true;
}
void ModList::setActive(const QModelIndexList& indices, bool active)
{
QList mods;
for (auto& index : indices) {
mods.append(index.data(IndexRole).toInt());
}
if (active) {
m_Profile->setModsEnabled(mods, {});
} else {
m_Profile->setModsEnabled({}, mods);
}
}