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 }