summaryrefslogtreecommitdiff
path: root/src/modlistsortproxy.cpp
diff options
context:
space:
mode:
authorMikaƫl Capelle <capelle.mikael@gmail.com>2023-07-09 17:36:03 +0200
committerGitHub <noreply@github.com>2023-07-09 17:36:03 +0200
commitef94aee28464039672b277243a0181ae93550d6c (patch)
tree0575a68af5f154e9d6d0738edbaefb16a11d6f5c /src/modlistsortproxy.cpp
parent7d6cb8528d20e36a4cee822263865ee2f7f32481 (diff)
parent3de050e9c03e553f7ae3f780f6bd080a30ae123e (diff)
Merge pull request #1839 from Holt59/ci/initial-gh-action-for-build
Clang-Format + Github Workflow
Diffstat (limited to 'src/modlistsortproxy.cpp')
-rw-r--r--src/modlistsortproxy.cpp1388
1 files changed, 683 insertions, 705 deletions
diff --git a/src/modlistsortproxy.cpp b/src/modlistsortproxy.cpp
index a34f730f..704ec10b 100644
--- a/src/modlistsortproxy.cpp
+++ b/src/modlistsortproxy.cpp
@@ -1,705 +1,683 @@
-/*
-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 "modlistsortproxy.h"
-#include "modlistbypriorityproxy.h"
-#include "modlistdropinfo.h"
-#include "modinfo.h"
-#include "profile.h"
-#include "messagedialog.h"
-#include "qtgroupingproxy.h"
-#include "organizercore.h"
-
-#include <log.h>
-#include <QMenu>
-#include <QCheckBox>
-#include <QWidgetAction>
-#include <QApplication>
-#include <QMimeData>
-#include <QDebug>
-#include <QTreeView>
-
-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>& 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<ModInfo::EFlag> &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<ModInfo::EConflictFlag>& 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<QtGroupingProxy*>(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<ModInfo::EFlag> leftFlags = leftMod->getFlags();
- std::vector<ModInfo::EFlag> 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<ModInfo::EConflictFlag> leftFlags = leftMod->getConflictFlags();
- std::vector<ModInfo::EConflictFlag> 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<unsigned int>(content.id());
- });
- m_Organizer->modDataContents().forEachContentIn(rContents, [&rValue](auto const& content) {
- rValue += 2U << static_cast<unsigned int>(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_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<ModInfo::EConflictFlag> &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 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->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<int>(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<ModListByPriorityProxy*>(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<QtGroupingProxy*>(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<QTreeView*>("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<QAbstractProxyModel*>(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();
- });
-}
-
+/*
+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 "modlistsortproxy.h"
+#include "messagedialog.h"
+#include "modinfo.h"
+#include "modlistbypriorityproxy.h"
+#include "modlistdropinfo.h"
+#include "organizercore.h"
+#include "profile.h"
+#include "qtgroupingproxy.h"
+
+#include <QApplication>
+#include <QCheckBox>
+#include <QDebug>
+#include <QMenu>
+#include <QMimeData>
+#include <QTreeView>
+#include <QWidgetAction>
+#include <log.h>
+
+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>& 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<ModInfo::EFlag>& 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<ModInfo::EConflictFlag>& 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<QtGroupingProxy*>(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<ModInfo::EFlag> leftFlags = leftMod->getFlags();
+ std::vector<ModInfo::EFlag> 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<ModInfo::EConflictFlag> leftFlags = leftMod->getConflictFlags();
+ std::vector<ModInfo::EConflictFlag> 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<unsigned int>(content.id());
+ });
+ m_Organizer->modDataContents().forEachContentIn(
+ rContents, [&rValue](auto const& content) {
+ rValue += 2U << static_cast<unsigned int>(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_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<ModInfo::EConflictFlag>& 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 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->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<int>(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<ModListByPriorityProxy*>(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<QtGroupingProxy*>(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<QTreeView*>("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<QAbstractProxyModel*>(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();
+ });
+}