/*
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 "modlistsortproxy.h"
#include "modinfo.h"
#include "profile.h"
#include "messagedialog.h"
#include "qtgroupingproxy.h"
#include
#include
#include
#include
#include
#include
#include
#include
using namespace MOBase;
ModListSortProxy::ModListSortProxy(Profile* profile, QObject *parent)
: QSortFilterProxyModel(parent)
, m_Profile(profile)
, m_FilterActive(false)
, m_FilterMode(FilterAnd)
, m_FilterSeparators(SeparatorFilter)
{
setDynamicSortFilter(true); // this seems to work without dynamicsortfilter
// but I don't know why. This should be necessary
}
void ModListSortProxy::setProfile(Profile *profile)
{
m_Profile = profile;
}
void ModListSortProxy::updateFilterActive()
{
m_FilterActive = (!m_Criteria.empty() || !m_Filter.isEmpty());
emit filterActive(m_FilterActive);
}
void ModListSortProxy::setCriteria(const std::vector& criteria)
{
// avoid refreshing the filter unless we are checking all mods for update.
const bool changed = (criteria != m_Criteria);
const bool isForUpdates = (
!criteria.empty() &&
criteria[0].id == CategoryFactory::UpdateAvailable);
if (changed || isForUpdates) {
m_Criteria = criteria;
updateFilterActive();
invalidate();
}
}
Qt::ItemFlags ModListSortProxy::flags(const QModelIndex &modelIndex) const
{
Qt::ItemFlags flags = sourceModel()->flags(mapToSource(modelIndex));
return flags;
}
void ModListSortProxy::enableAllVisible()
{
if (m_Profile == nullptr) return;
QList modsToEnable;
for (int i = 0; i < this->rowCount(); ++i) {
int modID = mapToSource(index(i, 0)).data(Qt::UserRole + 1).toInt();
modsToEnable.append(modID);
}
m_Profile->setModsEnabled(modsToEnable, QList());
invalidate();
}
void ModListSortProxy::disableAllVisible()
{
if (m_Profile == nullptr) return;
QList modsToDisable;
for (int i = 0; i < this->rowCount(); ++i) {
int modID = mapToSource(index(i, 0)).data(Qt::UserRole + 1).toInt();
modsToDisable.append(modID);
}
m_Profile->setModsEnabled(QList(), modsToDisable);
invalidate();
}
unsigned long ModListSortProxy::flagsId(const std::vector &flags) const
{
unsigned long result = 0;
for (ModInfo::EFlag flag : flags) {
if ((flag != ModInfo::FLAG_FOREIGN)
&& (flag != ModInfo::FLAG_OVERWRITE)) {
result += 1 << (int)flag;
}
}
return result;
}
unsigned long ModListSortProxy::conflictFlagsId(const std::vector& flags) const
{
unsigned long result = 0;
for (ModInfo::EConflictFlag flag : flags) {
if ((flag != ModInfo::FLAG_OVERWRITE_CONFLICT)) {
result += 1 << (int)flag;
}
}
return result;
}
bool ModListSortProxy::lessThan(const QModelIndex &left,
const QModelIndex &right) const
{
if (sourceModel()->hasChildren(left) || sourceModel()->hasChildren(right)) {
return QSortFilterProxyModel::lessThan(left, right);
}
bool lOk, rOk;
int leftIndex = left.data(Qt::UserRole + 1).toInt(&lOk);
int rightIndex = right.data(Qt::UserRole + 1).toInt(&rOk);
if (!lOk || !rOk) {
return false;
}
ModInfo::Ptr leftMod = ModInfo::getByIndex(leftIndex);
ModInfo::Ptr rightMod = ModInfo::getByIndex(rightIndex);
bool lt = false;
{
QModelIndex leftPrioIdx = left.sibling(left.row(), ModList::COL_PRIORITY);
QVariant leftPrio = leftPrioIdx.data();
if (!leftPrio.isValid()) leftPrio = left.data(Qt::UserRole);
QModelIndex rightPrioIdx = right.sibling(right.row(), ModList::COL_PRIORITY);
QVariant rightPrio = rightPrioIdx.data();
if (!rightPrio.isValid()) rightPrio = right.data(Qt::UserRole);
lt = leftPrio.toInt() < rightPrio.toInt();
}
switch (left.column()) {
case ModList::COL_FLAGS: {
std::vector leftFlags = leftMod->getFlags();
std::vector rightFlags = rightMod->getFlags();
if (leftFlags.size() != rightFlags.size()) {
lt = leftFlags.size() < rightFlags.size();
} else {
lt = flagsId(leftFlags) < flagsId(rightFlags);
}
} break;
case ModList::COL_CONFLICTFLAGS: {
std::vector leftFlags = leftMod->getConflictFlags();
std::vector rightFlags = rightMod->getConflictFlags();
if (leftFlags.size() != rightFlags.size()) {
lt = leftFlags.size() < rightFlags.size();
}
else {
lt = conflictFlagsId(leftFlags) < conflictFlagsId(rightFlags);
}
} break;
case ModList::COL_CONTENT: {
std::vector lContent = leftMod->getContents();
std::vector rContent = rightMod->getContents();
if (lContent.size() != rContent.size()) {
lt = lContent.size() < rContent.size();
}
int lValue = 0;
int rValue = 0;
for (ModInfo::EContent content : lContent) {
lValue += 2 << (unsigned int)content;
}
for (ModInfo::EContent content : rContent) {
rValue += 2 << (unsigned int)content;
}
lt = lValue < rValue;
} break;
case ModList::COL_NAME: {
int comp = QString::compare(leftMod->name(), rightMod->name(), Qt::CaseInsensitive);
if (comp != 0)
lt = comp < 0;
} break;
case ModList::COL_CATEGORY: {
if (leftMod->getPrimaryCategory() != rightMod->getPrimaryCategory()) {
if (leftMod->getPrimaryCategory() < 0) lt = false;
else if (rightMod->getPrimaryCategory() < 0) lt = true;
else {
try {
CategoryFactory &categories = CategoryFactory::instance();
QString leftCatName = categories.getCategoryName(categories.getCategoryIndex(leftMod->getPrimaryCategory()));
QString rightCatName = categories.getCategoryName(categories.getCategoryIndex(rightMod->getPrimaryCategory()));
lt = leftCatName < rightCatName;
} catch (const std::exception &e) {
log::error("failed to compare categories: {}", e.what());
}
}
}
} break;
case ModList::COL_MODID: {
if (leftMod->getNexusID() != rightMod->getNexusID())
lt = leftMod->getNexusID() < rightMod->getNexusID();
} break;
case ModList::COL_VERSION: {
if (leftMod->getVersion() != rightMod->getVersion())
lt = leftMod->getVersion() < rightMod->getVersion();
} break;
case ModList::COL_INSTALLTIME: {
QDateTime leftTime = left.data().toDateTime();
QDateTime rightTime = right.data().toDateTime();
if (leftTime != rightTime)
return leftTime < rightTime;
} break;
case ModList::COL_GAME: {
if (leftMod->getGameName() != rightMod->getGameName()) {
lt = leftMod->getGameName() < rightMod->getGameName();
}
else {
int comp = QString::compare(leftMod->name(), rightMod->name(), Qt::CaseInsensitive);
if (comp != 0)
lt = comp < 0;
}
} break;
case ModList::COL_NOTES: {
QString leftComments = leftMod->comments();
QString rightComments = rightMod->comments();
if (leftComments != rightComments) {
if (leftComments.isEmpty()) {
lt = sortOrder() == Qt::DescendingOrder;
} else if (rightComments.isEmpty()) {
lt = sortOrder() == Qt::AscendingOrder;
} else {
lt = leftComments < rightComments;
}
}
} break;
case ModList::COL_PRIORITY: {
// nop, already compared by priority
} break;
default: {
log::warn("Sorting is not defined for column {}", left.column());
} break;
}
return lt;
}
void ModListSortProxy::updateFilter(const QString& filter)
{
m_Filter = filter;
updateFilterActive();
invalidate();
}
bool ModListSortProxy::hasConflictFlag(const std::vector &flags) const
{
for (ModInfo::EFlag flag : flags) {
if ((flag == ModInfo::FLAG_CONFLICT_MIXED) ||
(flag == ModInfo::FLAG_CONFLICT_OVERWRITE) ||
(flag == ModInfo::FLAG_CONFLICT_OVERWRITTEN) ||
(flag == ModInfo::FLAG_CONFLICT_REDUNDANT) ||
(flag == ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE) ||
(flag == ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN) ||
(flag == ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED) ||
(flag == ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE) ||
(flag == ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN)) {
return true;
}
}
return false;
}
bool ModListSortProxy::filterMatchesModAnd(ModInfo::Ptr info, bool enabled) const
{
for (auto&& c : m_Criteria) {
if (!criteriaMatchMod(info, enabled, c)) {
return false;
}
}
return true;
}
bool ModListSortProxy::filterMatchesModOr(ModInfo::Ptr info, bool enabled) const
{
for (auto&& c : m_Criteria) {
if (criteriaMatchMod(info, enabled, c)) {
return true;
}
}
if (!m_Criteria.empty()) {
// nothing matched
return false;
}
return true;
}
bool ModListSortProxy::optionsMatchMod(ModInfo::Ptr info, bool) const
{
return true;
}
bool ModListSortProxy::criteriaMatchMod(
ModInfo::Ptr info, bool enabled, const Criteria& c) const
{
bool b = false;
switch (c.type)
{
case TypeSpecial: // fall-through
case TypeCategory:
{
b = categoryMatchesMod(info, enabled, c.id);
break;
}
case TypeContent:
{
b = contentMatchesMod(info, enabled, c.id);
break;
}
default:
{
log::error("bad criteria type {}", c.type);
break;
}
}
if (c.inverse) {
b = !b;
}
return b;
}
bool ModListSortProxy::categoryMatchesMod(
ModInfo::Ptr info, bool enabled, int category) const
{
bool b = false;
switch (category)
{
case CategoryFactory::Checked:
{
b = (enabled || info->alwaysEnabled());
break;
}
case CategoryFactory::UpdateAvailable:
{
b = (info->updateAvailable() || info->downgradeAvailable());
break;
}
case CategoryFactory::HasCategory:
{
b = !info->getCategories().empty();
break;
}
case CategoryFactory::Conflict:
{
b = (hasConflictFlag(info->getFlags()));
break;
}
case CategoryFactory::Endorsed:
{
b = (info->endorsedState() == ModInfo::ENDORSED_TRUE);
break;
}
case CategoryFactory::Backup:
{
b = (info->hasFlag(ModInfo::FLAG_BACKUP));
break;
}
case CategoryFactory::Managed:
{
b = (!info->hasFlag(ModInfo::FLAG_FOREIGN));
break;
}
case CategoryFactory::HasGameData:
{
b = !info->hasFlag(ModInfo::FLAG_INVALID);
break;
}
case CategoryFactory::HasNexusID:
{
// never show these
if (
info->hasFlag(ModInfo::FLAG_FOREIGN) ||
info->hasFlag(ModInfo::FLAG_BACKUP) ||
info->hasFlag(ModInfo::FLAG_OVERWRITE))
{
return false;
}
b = (info->getNexusID() > 0);
break;
}
case CategoryFactory::Tracked:
{
b = (info->trackedState() == ModInfo::TRACKED_TRUE);
break;
}
default:
{
b = (info->categorySet(category));
break;
}
}
return b;
}
bool ModListSortProxy::contentMatchesMod(ModInfo::Ptr info, bool enabled, int content) const
{
return info->hasContent(static_cast(content));
}
bool ModListSortProxy::filterMatchesMod(ModInfo::Ptr info, bool enabled) const
{
// don't check if there are no filters selected
if (!m_FilterActive) {
return true;
}
// special case for separators
if (info->hasFlag(ModInfo::FLAG_SEPARATOR)) {
switch (m_FilterSeparators)
{
case SeparatorFilter:
{
// filter normally
break;
}
case SeparatorShow:
{
// force visible
return true;
}
case SeparatorHide:
{
// force hide
return false;
}
}
}
if (!m_Filter.isEmpty()) {
bool display = false;
QString filterCopy = QString(m_Filter);
filterCopy.replace("||", ";").replace("OR", ";").replace("|", ";");
QStringList ORList = filterCopy.split(";", QString::SkipEmptyParts);
bool segmentGood = true;
//split in ORSegments that internally use AND logic
for (auto& ORSegment : ORList) {
QStringList ANDKeywords = ORSegment.split(" ", QString::SkipEmptyParts);
segmentGood = true;
bool foundKeyword = false;
//check each word in the segment for match, each word needs to be matched but it doesn't matter where.
for (auto& currentKeyword : ANDKeywords) {
foundKeyword = false;
//search keyword in name
if (m_EnabledColumns[ModList::COL_NAME] &&
info->name().contains(currentKeyword, Qt::CaseInsensitive)) {
foundKeyword = true;
}
// Search by notes
if (!foundKeyword &&
m_EnabledColumns[ModList::COL_NOTES] &&
info->comments().contains(currentKeyword, Qt::CaseInsensitive)) {
foundKeyword = true;
}
// Search by categories
if (!foundKeyword &&
m_EnabledColumns[ModList::COL_CATEGORY]) {
for (auto category : info->categories()) {
if (category.contains(currentKeyword, Qt::CaseInsensitive)) {
foundKeyword = true;
break;
}
}
}
// Search by Nexus ID
if (!foundKeyword &&
m_EnabledColumns[ModList::COL_MODID]) {
bool ok;
int filterID = currentKeyword.toInt(&ok);
if (ok) {
int modID = info->getNexusID();
while (modID > 0) {
if (modID == filterID) {
foundKeyword = true;
break;
}
modID = (int)(modID / 10);
}
}
}
if (!foundKeyword) {
//currentKeword is missing from everything, AND fails and we need to check next ORsegment
segmentGood = false;
break;
}
}//for ANDKeywords loop
if (segmentGood) {
//the last AND loop didn't break so the ORSegments is true so mod matches filter
display = true;
break;
}
}//for ORList loop
if (!display) {
return false;
}
}//if (!m_CurrentFilter.isEmpty())
if (m_FilterMode == FilterAnd) {
return filterMatchesModAnd(info, enabled);
}
else {
return filterMatchesModOr(info, enabled);
}
}
void ModListSortProxy::setColumnVisible(int column, bool visible)
{
m_EnabledColumns[column] = visible;
}
void ModListSortProxy::setOptions(
ModListSortProxy::FilterMode mode, SeparatorsMode separators)
{
if (m_FilterMode != mode || separators != m_FilterSeparators) {
m_FilterMode = mode;
m_FilterSeparators = separators;
this->invalidate();
}
}
bool ModListSortProxy::filterAcceptsRow(int row, const QModelIndex &parent) const
{
if (m_Profile == nullptr) {
return false;
}
if (row >= static_cast(m_Profile->numMods())) {
log::warn("invalid row index: {}", row);
return false;
}
QModelIndex idx = sourceModel()->index(row, 0, parent);
if (!idx.isValid()) {
log::debug("invalid mod index");
return false;
}
if (sourceModel()->hasChildren(idx)) {
for (int i = 0; i < sourceModel()->rowCount(idx); ++i) {
if (filterAcceptsRow(i, idx)) {
return true;
}
}
return false;
} else {
bool modEnabled = idx.sibling(row, 0).data(Qt::CheckStateRole).toInt() == Qt::Checked;
unsigned int index = idx.data(Qt::UserRole + 1).toInt();
return filterMatchesMod(ModInfo::getByIndex(index), modEnabled);
}
}
bool ModListSortProxy::dropMimeData(const QMimeData *data, Qt::DropAction action,
int row, int column, const QModelIndex &parent)
{
if (!data->hasUrls() && (sortColumn() != ModList::COL_PRIORITY)) {
QWidget *wid = qApp->activeWindow()->findChild("modList");
MessageDialog::showMessage(tr("Drag&Drop is only supported when sorting by priority"), wid);
return false;
}
if ((row == -1) && (column == -1)) {
return sourceModel()->dropMimeData(data, action, -1, -1, mapToSource(parent));
}
// in the regular model, when dropping between rows, the row-value passed to
// the sourceModel is inconsistent between ascending and descending ordering.
// This should fix that
if (sortOrder() == Qt::DescendingOrder) {
--row;
}
QModelIndex proxyIndex = index(row, column, parent);
QModelIndex sourceIndex = mapToSource(proxyIndex);
return this->sourceModel()->dropMimeData(data, action, sourceIndex.row(), sourceIndex.column(),
sourceIndex.parent());
}
void ModListSortProxy::setSourceModel(QAbstractItemModel *sourceModel)
{
QSortFilterProxyModel::setSourceModel(sourceModel);
QtGroupingProxy *proxy = qobject_cast(sourceModel);
if (proxy != nullptr) {
sourceModel = proxy->sourceModel();
}
connect(sourceModel, SIGNAL(aboutToChangeData()), this, SLOT(aboutToChangeData()), Qt::UniqueConnection);
connect(sourceModel, SIGNAL(postDataChanged()), this, SLOT(postDataChanged()), Qt::UniqueConnection);
}
void ModListSortProxy::aboutToChangeData()
{
// having a filter active when dataChanged is called caused a crash
// (at least with some Qt versions)
// this may be related to the fact that the item being edited may disappear from the view as a
// result of the edit
m_PreChangeCriteria = m_Criteria;
setCriteria({});
}
void ModListSortProxy::postDataChanged()
{
// if the filter is re-activated right away the editor can't be deleted but becomes invisible
// or at least the view continues to think it's being edited. As a result no new editor can be
// opened
QTimer::singleShot(10, [this] () {
setCriteria(m_PreChangeCriteria);
m_PreChangeCriteria.clear();
});
}