/*
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 "messagedialog.h"
#include "modinfo.h"
#include "modlistbypriorityproxy.h"
#include "modlistdropinfo.h"
#include "organizercore.h"
#include "profile.h"
#include "qtgroupingproxy.h"
#include
#include
#include
#include
#include
#include
#include
#include
using namespace MOBase;
ModListSortProxy::ModListSortProxy(Profile* profile, OrganizerCore* organizer)
: QSortFilterProxyModel(organizer), m_Organizer(organizer), 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();
invalidateFilter();
emit filterInvalidated();
}
}
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)) {
// when sorting by priority, we do not want to use the parent lessThan because
// it uses the display role which can be inconsistent (e.g. for backups)
if (sortColumn() != ModList::COL_PRIORITY) {
return QSortFilterProxyModel::lessThan(left, right);
} else if (qobject_cast(sourceModel())) {
// if the underlying proxy is a QtGroupingProxy we need to rely on
// Qt::DisplayRole because the other roles are not correctly handled
// by that kind of proxy
return left.data(Qt::DisplayRole).toInt() < right.data(Qt::DisplayRole).toInt();
}
}
bool lOk, rOk;
int leftIndex = left.data(ModList::IndexRole).toInt(&lOk);
int rightIndex = right.data(ModList::IndexRole).toInt(&rOk);
if (!lOk || !rOk) {
return false;
}
ModInfo::Ptr leftMod = ModInfo::getByIndex(leftIndex);
ModInfo::Ptr rightMod = ModInfo::getByIndex(rightIndex);
bool lt = left.data(ModList::PriorityRole).toInt() <
right.data(ModList::PriorityRole).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: {
const auto& lContents = leftMod->getContents();
const auto& rContents = rightMod->getContents();
unsigned int lValue = 0;
unsigned int rValue = 0;
m_Organizer->modDataContents().forEachContentIn(
lContents, [&lValue](auto const& content) {
lValue += 2U << static_cast(content.id());
});
m_Organizer->modDataContents().forEachContentIn(
rContents, [&rValue](auto const& content) {
rValue += 2U << static_cast(content.id());
});
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->primaryCategory() != rightMod->primaryCategory()) {
if (leftMod->primaryCategory() < 0)
lt = false;
else if (rightMod->primaryCategory() < 0)
lt = true;
else {
try {
CategoryFactory& categories = CategoryFactory::instance();
QString leftCatName = categories.getCategoryName(
categories.getCategoryIndex(leftMod->primaryCategory()));
QString rightCatName = categories.getCategoryName(
categories.getCategoryIndex(rightMod->primaryCategory()));
lt = leftCatName < rightCatName;
} catch (const std::exception& e) {
log::error("failed to compare categories: {}", e.what());
}
}
}
} break;
case ModList::COL_AUTHOR: {
if (leftMod->author() != rightMod->author()) {
lt = leftMod->author() < rightMod->author();
} else {
int comp = QString::compare(leftMod->uploader(), rightMod->uploader(),
Qt::CaseInsensitive);
if (comp != 0)
lt = comp < 0;
}
} break;
case ModList::COL_UPLOADER: {
if (leftMod->uploader() != rightMod->uploader()) {
lt = leftMod->uploader() < rightMod->uploader();
} else {
int comp = QString::compare(leftMod->uploader(), rightMod->uploader(),
Qt::CaseInsensitive);
if (comp != 0)
lt = comp < 0;
}
} break;
case ModList::COL_MODID: {
if (leftMod->nexusId() != rightMod->nexusId())
lt = leftMod->nexusId() < rightMod->nexusId();
} break;
case ModList::COL_VERSION: {
if (leftMod->version() != rightMod->version())
lt = leftMod->version() < rightMod->version();
} 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->gameName() != rightMod->gameName()) {
lt = leftMod->gameName() < rightMod->gameName();
} 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: {
if (leftMod->isBackup() != rightMod->isBackup()) {
lt = leftMod->isBackup();
} else if (leftMod->isOverwrite() != rightMod->isOverwrite()) {
lt = rightMod->isOverwrite();
}
} 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();
invalidateFilter();
emit filterInvalidated();
}
bool ModListSortProxy::hasConflictFlag(
const std::vector& flags) const
{
for (ModInfo::EConflictFlag 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->getConflictFlags()));
break;
}
case CategoryFactory::HasHiddenFiles: {
b = (info->hasFlag(ModInfo::FLAG_HIDDEN_FILES));
break;
}
case CategoryFactory::Endorsed: {
b = (info->endorsedState() == EndorsedState::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->nexusId() > 0);
break;
}
case CategoryFactory::Tracked: {
b = (info->trackedState() == TrackedState::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(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(";", Qt::SkipEmptyParts);
bool segmentGood = true;
// split in ORSegments that internally use AND logic
for (auto& ORSegment : ORList) {
QStringList ANDKeywords = ORSegment.split(" ", Qt::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 author
if (!foundKeyword && m_EnabledColumns[ModList::COL_AUTHOR] &&
info->author().contains(currentKeyword, Qt::CaseInsensitive)) {
foundKeyword = true;
}
// Search by uploader
if (!foundKeyword && m_EnabledColumns[ModList::COL_UPLOADER] &&
info->uploader().contains(currentKeyword, Qt::CaseInsensitive)) {
foundKeyword = true;
}
// Search by notes
if (!foundKeyword && m_EnabledColumns[ModList::COL_NOTES] &&
(info->notes().contains(currentKeyword, Qt::CaseInsensitive) ||
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->nexusId();
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;
invalidateFilter();
emit filterInvalidated();
}
}
bool ModListSortProxy::filterAcceptsRow(int source_row, const QModelIndex& parent) const
{
if (m_Profile == nullptr) {
return false;
}
if (source_row >= static_cast(m_Profile->numMods())) {
log::warn("invalid row index: {}", source_row);
return false;
}
QModelIndex idx = sourceModel()->index(source_row, 0, parent);
if (!idx.isValid()) {
log::debug("invalid mod index");
return false;
}
unsigned int index = ULONG_MAX;
{
bool ok = false;
index = idx.data(ModList::IndexRole).toInt(&ok);
if (!ok) {
index = ULONG_MAX;
}
}
if (sourceModel()->hasChildren(idx)) {
// we need to check the separator itself first
if (index < ModInfo::getNumMods() && ModInfo::getByIndex(index)->isSeparator()) {
if (filterMatchesMod(ModInfo::getByIndex(index), false)) {
return true;
}
}
for (int i = 0; i < sourceModel()->rowCount(idx); ++i) {
if (filterAcceptsRow(i, idx)) {
return true;
}
}
return false;
} else {
bool modEnabled =
idx.sibling(source_row, 0).data(Qt::CheckStateRole).toInt() == Qt::Checked;
return filterMatchesMod(ModInfo::getByIndex(index), modEnabled);
}
}
bool ModListSortProxy::sourceIsByPriorityProxy() const
{
return dynamic_cast(sourceModel()) != nullptr;
}
bool ModListSortProxy::canDropMimeData(const QMimeData* data, Qt::DropAction action,
int row, int column,
const QModelIndex& parent) const
{
ModListDropInfo dropInfo(data, *m_Organizer);
if (!dropInfo.isLocalFileDrop() && sortColumn() != ModList::COL_PRIORITY) {
return false;
}
// disable drop install with group proxy, except the one for collapsible separator
// - it would be nice to be able to "install to category" or something like that but
// it's a bit more complicated since the drop position is based on the category,
// so just disabling for now
if (dropInfo.isDownloadDrop()) {
// maybe there is a cleaner way?
if (qobject_cast(sourceModel())) {
return false;
}
}
// see dropMimeData for details
if (sortOrder() == Qt::DescendingOrder && row != -1 && !sourceIsByPriorityProxy()) {
--row;
}
return QSortFilterProxyModel::canDropMimeData(data, action, row, column, parent);
}
bool ModListSortProxy::dropMimeData(const QMimeData* data, Qt::DropAction action,
int row, int column, const QModelIndex& parent)
{
ModListDropInfo dropInfo(data, *m_Organizer);
if (!dropInfo.isLocalFileDrop() && 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
//
// we want to fix that, but we cannot do it for the by-priority proxy because
// it messes up with non top-level items, so we simply forward the row and the
// by-priority proxy will fix the row for us
if (sortOrder() == Qt::DescendingOrder && row != -1 && !sourceIsByPriorityProxy()) {
--row;
}
return QSortFilterProxyModel::dropMimeData(data, action, row, column, parent);
}
void ModListSortProxy::setSourceModel(QAbstractItemModel* sourceModel)
{
QSortFilterProxyModel::setSourceModel(sourceModel);
QAbstractProxyModel* proxy = qobject_cast(sourceModel);
if (proxy != nullptr) {
sourceModel = proxy->sourceModel();
}
if (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();
});
}