summaryrefslogtreecommitdiff
path: root/src/filterlist.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/filterlist.cpp')
-rw-r--r--src/filterlist.cpp419
1 files changed, 419 insertions, 0 deletions
diff --git a/src/filterlist.cpp b/src/filterlist.cpp
new file mode 100644
index 00000000..69aca4c5
--- /dev/null
+++ b/src/filterlist.cpp
@@ -0,0 +1,419 @@
+#include "filterlist.h"
+#include "ui_mainwindow.h"
+#include "categories.h"
+#include "categoriesdialog.h"
+#include "settings.h"
+#include <utility.h>
+
+using namespace MOBase;
+using CriteriaType = ModListSortProxy::CriteriaType;
+using Criteria = ModListSortProxy::Criteria;
+
+class FilterList::CriteriaItem : public QTreeWidgetItem
+{
+public:
+ enum States
+ {
+ FirstState = 0,
+
+ Inactive = FirstState,
+ Active,
+ Inverted,
+
+ LastState = Inverted
+ };
+
+ CriteriaItem(FilterList* list, QString name, CriteriaType type, int id)
+ : QTreeWidgetItem({"", name}), m_list(list), m_state(Inactive)
+ {
+ setData(0, Qt::ToolTipRole, name);
+ setData(0, TypeRole, type);
+ setData(0, IDRole, id);
+ }
+
+ CriteriaType type() const
+ {
+ return static_cast<CriteriaType>(data(0, TypeRole).toInt());
+ }
+
+ int id() const
+ {
+ return data(0, IDRole).toInt();
+ }
+
+ States state() const
+ {
+ return m_state;
+ }
+
+ void setState(States s)
+ {
+ if (m_state != s) {
+ m_state = s;
+ updateState();
+ }
+ }
+
+ void nextState()
+ {
+ m_state = static_cast<States>(m_state + 1);
+ if (m_state > LastState) {
+ m_state = FirstState;
+ }
+
+ updateState();
+ }
+
+ void previousState()
+ {
+ m_state = static_cast<States>(m_state - 1);
+ if (m_state < FirstState) {
+ m_state = LastState;
+ }
+
+ updateState();
+ }
+
+private:
+ const int IDRole = Qt::UserRole;
+ const int TypeRole = Qt::UserRole + 1;
+
+ FilterList* m_list;
+ States m_state;
+
+ void updateState()
+ {
+ QString s;
+
+ switch (m_state)
+ {
+ case Inactive:
+ {
+ break;
+ }
+
+ case Active:
+ {
+ // U+2713 CHECK MARK
+ s = QString::fromUtf8("\xe2\x9c\x93");
+ break;
+ }
+
+ case Inverted:
+ {
+ s = tr("Not");
+ break;
+ }
+ }
+
+ setText(0, s);
+ }
+};
+
+
+class CriteriaItemFilter : public QObject
+{
+public:
+ using Callback = std::function<bool (QTreeWidgetItem*, int)>;
+
+ CriteriaItemFilter(QTreeWidget* tree, Callback f)
+ : QObject(tree), m_tree(tree), m_f(std::move(f))
+ {
+ }
+
+ bool eventFilter(QObject* o, QEvent* e) override
+ {
+ // careful: this filter is installed on both the tree and the viewport
+ //
+ // no check is currently necessary because mouse events originate from the
+ // viewport only and keyboard events from the tree only
+
+ if (m_f) {
+ if (e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonDblClick) {
+ if (handleMouse(static_cast<QMouseEvent*>(e))) {
+ return true;
+ }
+ } else if (e->type() == QEvent::KeyPress) {
+ if (handleKeyboard(static_cast<QKeyEvent*>(e))) {
+ return true;
+ }
+ }
+ }
+
+ return QObject::eventFilter(o, e);
+ }
+
+private:
+ QTreeWidget* m_tree;
+ Callback m_f;
+
+ bool handleMouse(QMouseEvent* e)
+ {
+ auto* item = m_tree->itemAt(e->pos());
+ if (!item) {
+ return false;
+ }
+
+ m_tree->setCurrentItem(item);
+
+ const auto dir = (e->button() == Qt::LeftButton ? 1 : - 1);
+
+ return m_f(item, dir);
+ }
+
+ bool handleKeyboard(QKeyEvent* e)
+ {
+ if (e->key() == Qt::Key_Space) {
+ auto* item = m_tree->currentItem();
+ if (!item) {
+ return false;
+ }
+
+ const auto shiftPressed = (e->modifiers() & Qt::ShiftModifier);
+ const auto dir = (shiftPressed ? -1 : 1);
+
+ return m_f(item, dir);
+ }
+
+ return false;
+ }
+};
+
+
+FilterList::FilterList(Ui::MainWindow* ui, CategoryFactory& factory)
+ : ui(ui), m_factory(factory)
+{
+ auto* eventFilter = new CriteriaItemFilter(
+ ui->filters, [&](auto* item, int dir){ return cycleItem(item, dir); });
+
+ ui->filters->installEventFilter(eventFilter);
+ ui->filters->viewport()->installEventFilter(eventFilter);
+
+ connect(ui->filtersClear, &QPushButton::clicked, [&]{ clearSelection(); });
+ connect(ui->filtersEdit, &QPushButton::clicked, [&]{ editCategories(); });
+ connect(ui->filtersAnd, &QCheckBox::toggled, [&]{ onOptionsChanged(); });
+ connect(ui->filtersOr, &QCheckBox::toggled, [&]{ onOptionsChanged(); });
+
+ connect(
+ ui->filtersSeparators, qOverload<int>(&QComboBox::currentIndexChanged),
+ [&]{ onOptionsChanged(); });
+
+ ui->filters->header()->setMinimumSectionSize(0);
+ ui->filters->header()->setSectionResizeMode(0, QHeaderView::Fixed);
+ ui->filters->header()->resizeSection(0, 30);
+ ui->categoriesSplitter->setCollapsible(0, false);
+ ui->categoriesSplitter->setCollapsible(1, false);
+
+ ui->filtersSeparators->addItem(tr("Filter separators"), ModListSortProxy::SeparatorFilter);
+ ui->filtersSeparators->addItem(tr("Show separators"), ModListSortProxy::SeparatorShow);
+ ui->filtersSeparators->addItem(tr("Hide separators"), ModListSortProxy::SeparatorHide);
+}
+
+void FilterList::restoreState(const Settings& s)
+{
+ s.widgets().restoreIndex(ui->filtersSeparators);
+}
+
+void FilterList::saveState(Settings& s) const
+{
+ s.widgets().saveIndex(ui->filtersSeparators);
+}
+
+QTreeWidgetItem* FilterList::addCriteriaItem(
+ QTreeWidgetItem *root, const QString &name, int categoryID,
+ CriteriaType type)
+{
+ auto* item = new CriteriaItem(this, name, type, categoryID);
+
+ if (root != nullptr) {
+ root->addChild(item);
+ } else {
+ ui->filters->addTopLevelItem(item);
+ }
+
+ item->setTextAlignment(0, Qt::AlignCenter);
+
+ return item;
+}
+
+void FilterList::addContentCriteria()
+{
+ for (unsigned i = 0; i < ModInfo::NUM_CONTENT_TYPES; ++i) {
+ QString filterName = tr("Contains %1").arg(ModInfo::getContentTypeName(i));
+ addCriteriaItem(
+ nullptr, QString("<%1>").arg(filterName),
+ i, ModListSortProxy::TypeContent);
+ }
+}
+
+void FilterList::addCategoryCriteria(QTreeWidgetItem *root, const std::set<int> &categoriesUsed, int targetID)
+{
+ const auto count = static_cast<unsigned int>(m_factory.numCategories());
+ for (unsigned int i = 1; i < count; ++i) {
+ if (m_factory.getParentID(i) == targetID) {
+ int categoryID = m_factory.getCategoryID(i);
+ if (categoriesUsed.find(categoryID) != categoriesUsed.end()) {
+ QTreeWidgetItem *item =
+ addCriteriaItem(root, m_factory.getCategoryName(i),
+ categoryID, ModListSortProxy::TypeCategory);
+ if (m_factory.hasChildren(i)) {
+ addCategoryCriteria(item, categoriesUsed, categoryID);
+ }
+ }
+ }
+ }
+}
+
+void FilterList::addSpecialCriteria(int type)
+{
+ const auto sc = static_cast<CategoryFactory::SpecialCategories>(type);
+
+ addCriteriaItem(
+ nullptr, m_factory.getSpecialCategoryName(sc),
+ type, ModListSortProxy::TypeSpecial);
+}
+
+void FilterList::refresh()
+{
+ const auto oldSelection = selectedCriteria();
+
+ ui->filters->clear();
+
+ using F = CategoryFactory;
+ addSpecialCriteria(F::Checked);
+ addSpecialCriteria(F::UpdateAvailable);
+ addSpecialCriteria(F::Backup);
+ addSpecialCriteria(F::Managed);
+ addSpecialCriteria(F::HasCategory);
+ addSpecialCriteria(F::Conflict);
+ addSpecialCriteria(F::Endorsed);
+ addSpecialCriteria(F::Tracked);
+ addSpecialCriteria(F::HasNexusID);
+ addSpecialCriteria(F::HasGameData);
+
+ addContentCriteria();
+
+ std::set<int> categoriesUsed;
+ for (unsigned int modIdx = 0; modIdx < ModInfo::getNumMods(); ++modIdx) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIdx);
+ for (int categoryID : modInfo->getCategories()) {
+ int currentID = categoryID;
+ std::set<int> cycleTest;
+ // also add parents so they show up in the tree
+ while (currentID != 0) {
+ categoriesUsed.insert(currentID);
+ if (!cycleTest.insert(currentID).second) {
+ log::warn("cycle in categories: {}", SetJoin(cycleTest, ", "));
+ break;
+ }
+ currentID = m_factory.getParentID(m_factory.getCategoryIndex(currentID));
+ }
+ }
+ }
+
+ addCategoryCriteria(nullptr, categoriesUsed, 0);
+ setSelection(oldSelection);
+}
+
+void FilterList::setSelection(const std::vector<Criteria>& criteria)
+{
+ for (int i = 0; i < ui->filters->topLevelItemCount(); ++i) {
+ auto* item = dynamic_cast<CriteriaItem*>(ui->filters->topLevelItem(i));
+ if (!item) {
+ continue;
+ }
+
+ bool found = false;
+
+ for (auto&& c : criteria) {
+ if (item->type() == c.type && item->id() == c.id) {
+ item->setState(c.inverse ? CriteriaItem::Inverted : CriteriaItem::Active);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ item->setState(CriteriaItem::Inactive);
+ }
+ }
+}
+
+void FilterList::clearSelection()
+{
+ for (int i=0; i<ui->filters->topLevelItemCount(); ++i) {
+ auto* ci = dynamic_cast<CriteriaItem*>(ui->filters->topLevelItem(i));
+ if (!ci) {
+ continue;
+ }
+
+ ci->setState(CriteriaItem::Inactive);
+ }
+
+ checkCriteria();
+}
+
+bool FilterList::cycleItem(QTreeWidgetItem* item, int direction)
+{
+ auto* ci = dynamic_cast<CriteriaItem*>(item);
+ if (!ci) {
+ return false;
+ }
+
+ if (direction > 0) {
+ ci->nextState();
+ } else if (direction < 0) {
+ ci->previousState();
+ } else {
+ return false;
+ }
+
+ checkCriteria();
+ return true;
+}
+
+std::vector<ModListSortProxy::Criteria> FilterList::selectedCriteria() const
+{
+ std::vector<Criteria> criteria;
+
+ for (int i=0; i<ui->filters->topLevelItemCount(); ++i) {
+ const auto* ci = dynamic_cast<CriteriaItem*>(ui->filters->topLevelItem(i));
+ if (!ci) {
+ continue;
+ }
+
+ if (ci->state() != CriteriaItem::Inactive) {
+ criteria.push_back({
+ ci->type(), ci->id(), (ci->state() == CriteriaItem::Inverted)
+ });
+ }
+ }
+
+ return criteria;
+}
+
+void FilterList::checkCriteria()
+{
+ emit criteriaChanged(selectedCriteria());
+}
+
+void FilterList::editCategories()
+{
+ CategoriesDialog dialog(qApp->activeWindow());
+
+ if (dialog.exec() == QDialog::Accepted) {
+ dialog.commitChanges();
+ refresh();
+ }
+}
+
+void FilterList::onOptionsChanged()
+{
+ const auto mode = ui->filtersAnd->isChecked() ?
+ ModListSortProxy::FilterAnd: ModListSortProxy::FilterOr;
+
+ const auto separators = static_cast<ModListSortProxy::SeparatorsMode>(
+ ui->filtersSeparators->currentData().toInt());
+
+ emit optionsChanged(mode, separators);
+}