File indexing completed on 2024-05-05 17:33:19

0001 /*
0002  *   SPDX-FileCopyrightText: 2010 Jonathan Thomas <echidnaman@kubuntu.org>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "Category.h"
0008 
0009 #include <QTimer>
0010 
0011 #include "libdiscover_debug.h"
0012 #include <KLocalizedString>
0013 #include <QFile>
0014 #include <QStandardPaths>
0015 #include <QXmlStreamReader>
0016 #include <utils.h>
0017 
0018 Category::Category(QSet<QString> pluginName, QObject *parent)
0019     : QObject(parent)
0020     , m_iconString(QStringLiteral("applications-other"))
0021     , m_plugins(std::move(pluginName))
0022 {
0023     // Use a timer so to compress the rootCategoriesChanged signal
0024     // It generally triggers when KNS is unavailable at large (as explained in bug 454442)
0025     m_subCategoriesChanged = new QTimer(this);
0026     m_subCategoriesChanged->setInterval(0);
0027     m_subCategoriesChanged->setSingleShot(true);
0028     connect(m_subCategoriesChanged, &QTimer::timeout, this, &Category::subCategoriesChanged);
0029 }
0030 
0031 Category::Category(const QString &name,
0032                    const QString &iconName,
0033                    const CategoryFilter &filter,
0034                    const QSet<QString> &pluginName,
0035                    const QVector<Category *> &subCategories,
0036                    bool isAddons)
0037     : QObject(nullptr)
0038     , m_name(name)
0039     , m_iconString(iconName)
0040     , m_filter(filter)
0041     , m_subCategories(subCategories)
0042     , m_plugins(pluginName)
0043     , m_isAddons(isAddons)
0044     , m_priority(isAddons ? 5 : 0)
0045 {
0046     setObjectName(m_name);
0047 
0048     // Use a timer so to compress the rootCategoriesChanged signal
0049     // It generally triggers when KNS is unavailable at large (as explained in bug 454442)
0050     m_subCategoriesChanged = new QTimer(this);
0051     m_subCategoriesChanged->setInterval(0);
0052     m_subCategoriesChanged->setSingleShot(true);
0053     connect(m_subCategoriesChanged, &QTimer::timeout, this, &Category::subCategoriesChanged);
0054 }
0055 
0056 Category::~Category() = default;
0057 
0058 void Category::parseData(const QString &path, QXmlStreamReader *xml)
0059 {
0060     Q_ASSERT(xml->name() == QLatin1String("Menu"));
0061     while (!xml->atEnd() && !xml->hasError()) {
0062         xml->readNext();
0063 
0064         if (xml->isEndElement() && xml->name() == QLatin1String("Menu")) {
0065             break;
0066         } else if (!xml->isStartElement()) {
0067             if (xml->isCharacters() && xml->text().trimmed().isEmpty())
0068                 ;
0069             else if (!xml->isComment())
0070                 qDebug() << "skipping" << xml->tokenString() << xml->name();
0071             continue;
0072         }
0073 
0074         if (xml->name() == QLatin1String("Name")) {
0075             m_untranslatedName = xml->readElementText();
0076             m_name = i18nc("Category", m_untranslatedName.toUtf8().constData());
0077             setObjectName(m_untranslatedName);
0078         } else if (xml->name() == QLatin1String("Menu")) {
0079             m_subCategories << new Category(m_plugins, this);
0080             m_subCategories.last()->parseData(path, xml);
0081         } else if (xml->name() == QLatin1String("Addons")) {
0082             m_isAddons = true;
0083             m_priority = 5;
0084             xml->readNext();
0085         } else if (xml->name() == QLatin1String("Icon")) {
0086             m_iconString = xml->readElementText();
0087         } else if (xml->name() == QLatin1String("Include")
0088                    || xml->name() == QLatin1String("Categories")) {
0089             const QString opening = xml->name().toString();
0090             while (!xml->atEnd() && !xml->hasError()) {
0091                 xml->readNext();
0092 
0093                 if (xml->isEndElement() && xml->name() == opening) {
0094                     qDebug() << "weird, let's go" << opening << xml->lineNumber();
0095                     break;
0096                 } else if (!xml->isStartElement()) {
0097                     if (xml->isCharacters() && xml->text().trimmed().isEmpty())
0098                         ;
0099                     else if (!xml->isComment())
0100                         qDebug() << "include skipping" << xml->tokenString() << xml->text() << xml->name() << opening << xml->lineNumber();
0101                     continue;
0102                 }
0103                 break;
0104             }
0105             m_filter = parseIncludes(xml);
0106 
0107             // Here we are at the end of the last item in the group, we need to finish what we started
0108             while (!xml->atEnd() && !xml->hasError()) {
0109                 xml->readNext();
0110 
0111                 if (xml->isEndElement() && xml->name() == opening) {
0112                     break;
0113                 } else {
0114                     if (xml->isCharacters() && xml->text().trimmed().isEmpty())
0115                         ;
0116                     else if (!xml->isComment())
0117                         qDebug() << "include2 skipping" << xml->tokenString() << xml->text() << xml->name() << opening << xml->lineNumber();
0118                     continue;
0119                 }
0120                 break;
0121             }
0122         } else if (xml->name() == QLatin1String("Top")) {
0123             xml->skipCurrentElement();
0124             m_priority = -5;
0125         } else {
0126             qDebug() << "unknown element" << xml->name();
0127             xml->skipCurrentElement();
0128         }
0129         Q_ASSERT(xml->isEndElement());
0130     }
0131     Q_ASSERT(xml->isEndElement() && xml->name() == QLatin1String("Menu"));
0132 }
0133 
0134 CategoryFilter Category::parseIncludes(QXmlStreamReader *xml)
0135 {
0136     const QString opening = xml->name().toString();
0137     Q_ASSERT(xml->isStartElement());
0138 
0139     auto subIncludes = [&]() {
0140         QVector<CategoryFilter> filters;
0141 
0142         Q_ASSERT(xml->isStartElement());
0143         const QString opening = xml->name().toString();
0144 
0145         while (!xml->atEnd() && !xml->hasError()) {
0146             xml->readNext();
0147 
0148             if (xml->isEndElement()) {
0149                 break;
0150             } else if (xml->isStartElement()) {
0151                 filters.append(parseIncludes(xml));
0152             }
0153         }
0154         Q_ASSERT(xml->isEndElement());
0155         Q_ASSERT(xml->name() == opening);
0156         return filters;
0157     };
0158 
0159     CategoryFilter filter;
0160     if (xml->name() == QLatin1String("And")) {
0161         filter = {CategoryFilter::AndFilter, subIncludes()};
0162     } else if (xml->name() == QLatin1String("Or")) {
0163         filter = {CategoryFilter::OrFilter, subIncludes()};
0164     } else if (xml->name() == QLatin1String("Not")) {
0165         filter = {CategoryFilter::NotFilter, subIncludes()};
0166     } else if (xml->name() == QLatin1String("PkgSection")) {
0167         filter = {CategoryFilter::PkgSectionFilter, xml->readElementText()};
0168     } else if (xml->name() == QLatin1String("Category")) {
0169         filter = {CategoryFilter::CategoryNameFilter, xml->readElementText()};
0170         Q_ASSERT(xml->isEndElement() && xml->name() == QLatin1String("Category"));
0171     } else if (xml->name() == QLatin1String("PkgWildcard")) {
0172         filter = {CategoryFilter::PkgWildcardFilter, xml->readElementText()};
0173     } else if (xml->name() == QLatin1String("AppstreamIdWildcard")) {
0174         filter = {CategoryFilter::AppstreamIdWildcardFilter, xml->readElementText()};
0175     } else if (xml->name() == QLatin1String("PkgName")) {
0176         filter = {CategoryFilter::PkgNameFilter, xml->readElementText()};
0177     } else {
0178         qCWarning(LIBDISCOVER_LOG) << "unknown" << xml->name() << xml->lineNumber();
0179     }
0180 
0181     Q_ASSERT(xml->isEndElement());
0182     Q_ASSERT(xml->name() == opening);
0183 
0184     return filter;
0185 }
0186 
0187 QString Category::name() const
0188 {
0189     return m_name;
0190 }
0191 
0192 void Category::setName(const QString &name)
0193 {
0194     m_name = name;
0195     Q_EMIT nameChanged();
0196 }
0197 
0198 QString Category::icon() const
0199 {
0200     return m_iconString;
0201 }
0202 
0203 CategoryFilter Category::filter() const
0204 {
0205     return m_filter;
0206 }
0207 
0208 void Category::setFilter(const CategoryFilter &filter)
0209 {
0210     m_filter = filter;
0211 }
0212 
0213 QVector<Category *> Category::subCategories() const
0214 {
0215     return m_subCategories;
0216 }
0217 
0218 bool Category::categoryLessThan(Category *c1, const Category *c2)
0219 {
0220     return (c1->priority() < c2->priority()) || (c1->priority() == c2->priority() && QString::localeAwareCompare(c1->name(), c2->name()) < 0);
0221 }
0222 
0223 static bool isSorted(const QVector<Category *> &vector)
0224 {
0225     Category *last = nullptr;
0226     for (auto a : vector) {
0227         if (last && !Category::categoryLessThan(last, a))
0228             return false;
0229         last = a;
0230     }
0231     return true;
0232 }
0233 
0234 void Category::sortCategories(QVector<Category *> &cats)
0235 {
0236     std::sort(cats.begin(), cats.end(), &categoryLessThan);
0237     for (auto cat : cats) {
0238         sortCategories(cat->m_subCategories);
0239     }
0240     Q_ASSERT(isSorted(cats));
0241 }
0242 
0243 QDebug operator<<(QDebug debug, const CategoryFilter &filter)
0244 {
0245     QDebugStateSaver saver(debug);
0246     debug.nospace() << "Filter(";
0247     debug << filter.type << ", ";
0248 
0249     if (auto x = std::get_if<QString>(&filter.value)) {
0250         debug << std::get<QString>(filter.value);
0251     } else {
0252         debug << std::get<QVector<CategoryFilter>>(filter.value);
0253     }
0254     debug.nospace() << ')';
0255     return debug;
0256 }
0257 
0258 void Category::addSubcategory(QVector<Category *> &list, Category *newcat)
0259 {
0260     Q_ASSERT(isSorted(list));
0261 
0262     auto it = std::lower_bound(list.begin(), list.end(), newcat, &categoryLessThan);
0263     if (it == list.end()) {
0264         list << newcat;
0265         return;
0266     }
0267 
0268     auto c = *it;
0269     if (c->name() == newcat->name()) {
0270         if (c->icon() != newcat->icon() || c->m_priority != newcat->m_priority) {
0271             qCWarning(LIBDISCOVER_LOG) << "the following categories seem to be the same but they're not entirely" << c->icon() << newcat->icon() << "--"
0272                                        << c->name() << newcat->name() << "--" << c->isAddons() << newcat->isAddons();
0273         } else {
0274             CategoryFilter newFilter = {CategoryFilter::OrFilter, QVector<CategoryFilter>{c->m_filter, newcat->m_filter}};
0275             c->m_filter = newFilter;
0276             c->m_plugins.unite(newcat->m_plugins);
0277             const auto subCategories = newcat->subCategories();
0278             for (Category *nc : subCategories) {
0279                 addSubcategory(c->m_subCategories, nc);
0280             }
0281             return;
0282         }
0283     }
0284 
0285     list.insert(it, newcat);
0286     Q_ASSERT(isSorted(list));
0287 }
0288 
0289 void Category::addSubcategory(Category *cat)
0290 {
0291     int i = 0;
0292     for (Category *subCat : qAsConst(m_subCategories)) {
0293         if (!categoryLessThan(subCat, cat)) {
0294             break;
0295         }
0296         ++i;
0297     }
0298     m_subCategories.insert(i, cat);
0299     Q_ASSERT(isSorted(m_subCategories));
0300 }
0301 
0302 bool Category::blacklistPluginsInVector(const QSet<QString> &pluginNames, QVector<Category *> &subCategories)
0303 {
0304     bool ret = false;
0305     for (QVector<Category *>::iterator it = subCategories.begin(); it != subCategories.end();) {
0306         if ((*it)->blacklistPlugins(pluginNames)) {
0307             delete *it;
0308             it = subCategories.erase(it);
0309             ret = true;
0310         } else
0311             ++it;
0312     }
0313     return ret;
0314 }
0315 
0316 bool Category::blacklistPlugins(const QSet<QString> &pluginNames)
0317 {
0318     if (m_plugins.subtract(pluginNames).isEmpty()) {
0319         return true;
0320     }
0321 
0322     if (blacklistPluginsInVector(pluginNames, m_subCategories)) {
0323         m_subCategoriesChanged->start();
0324     }
0325     return false;
0326 }
0327 
0328 QVariantList Category::subCategoriesVariant() const
0329 {
0330     return kTransform<QVariantList>(m_subCategories, [](Category *cat) {
0331         return QVariant::fromValue<QObject *>(cat);
0332     });
0333 }
0334 
0335 bool Category::matchesCategoryName(const QString &name) const
0336 {
0337     return involvedCategories().contains(name);
0338 }
0339 
0340 bool Category::contains(Category *cat) const
0341 {
0342     const bool ret = cat == this || (cat && contains(qobject_cast<Category *>(cat->parent())));
0343     return ret;
0344 }
0345 
0346 bool Category::contains(const QVariantList &cats) const
0347 {
0348     bool ret = false;
0349     for (const auto &itCat : cats) {
0350         if (contains(qobject_cast<Category *>(itCat.value<QObject *>()))) {
0351             ret = true;
0352             break;
0353         }
0354     }
0355     return ret;
0356 }
0357 
0358 static QStringList involvedCategories(const CategoryFilter &f)
0359 {
0360     switch (f.type) {
0361     case CategoryFilter::CategoryNameFilter:
0362         return {std::get<QString>(f.value)};
0363     case CategoryFilter::OrFilter:
0364     case CategoryFilter::AndFilter: {
0365         const auto filters = std::get<QVector<CategoryFilter>>(f.value);
0366         QStringList ret;
0367         ret.reserve(filters.size());
0368         for (const auto &subFilters : filters) {
0369             ret << involvedCategories(subFilters);
0370         }
0371         ret.removeDuplicates();
0372         return ret;
0373     } break;
0374     case CategoryFilter::AppstreamIdWildcardFilter:
0375     case CategoryFilter::NotFilter:
0376     case CategoryFilter::PkgSectionFilter:
0377     case CategoryFilter::PkgWildcardFilter:
0378     case CategoryFilter::PkgNameFilter:
0379         break;
0380     }
0381     qCWarning(LIBDISCOVER_LOG) << "cannot infer categories from" << f.type;
0382     return {};
0383 }
0384 
0385 QStringList Category::involvedCategories() const
0386 {
0387     return ::involvedCategories(m_filter);
0388 }
0389 
0390 bool CategoryFilter::operator==(const CategoryFilter &other) const
0391 {
0392     if (other.type != type) {
0393         return false;
0394     }
0395 
0396     if (auto x = std::get_if<QString>(&value)) {
0397         return *x == std::get<QString>(other.value);
0398     } else {
0399         return std::get<QVector<CategoryFilter>>(value) == std::get<QVector<CategoryFilter>>(other.value);
0400     }
0401 }