File indexing completed on 2024-11-24 05:01:58

0001 /*
0002     SPDX-FileCopyrightText: 2007 Ivan Cukic <ivan.cukic+kde@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "plasmaappletitemmodel_p.h"
0008 
0009 #include <QFileInfo>
0010 #include <QMimeData>
0011 #include <QStandardPaths>
0012 #include <QVersionNumber>
0013 
0014 #include "config-workspace.h"
0015 #include <KAboutData>
0016 #include <KConfig>
0017 #include <KJsonUtils>
0018 #include <KLocalizedString>
0019 #include <KPackage/PackageLoader>
0020 #include <KRuntimePlatform>
0021 
0022 PlasmaAppletItem::PlasmaAppletItem(const KPluginMetaData &info)
0023     : AbstractItem()
0024     , m_info(info)
0025     , m_runningCount(0)
0026     , m_local(false)
0027 {
0028     const QString _f = PLASMA_RELATIVE_DATA_INSTALL_DIR "/plasmoids/" + info.pluginId() + '/';
0029     QFileInfo dir(QStandardPaths::locate(QStandardPaths::QStandardPaths::GenericDataLocation, _f, QStandardPaths::LocateDirectory));
0030     m_local = dir.exists() && dir.isWritable();
0031 
0032     setText(m_info.name() + " - " + m_info.category().toLower());
0033 
0034     if (QIcon::hasThemeIcon(info.pluginId())) {
0035         setIcon(QIcon::fromTheme(info.pluginId()));
0036     } else if (!m_info.iconName().isEmpty()) {
0037         setIcon(QIcon::fromTheme(info.iconName()));
0038     } else {
0039         setIcon(QIcon::fromTheme(QStringLiteral("application-x-plasma")));
0040     }
0041 
0042     // set plugininfo parts as roles in the model, only way qml can understand it
0043     setData(name(), PlasmaAppletItemModel::NameRole);
0044     setData(pluginName(), PlasmaAppletItemModel::PluginNameRole);
0045     setData(description(), PlasmaAppletItemModel::DescriptionRole);
0046     setData(category().toLower(), PlasmaAppletItemModel::CategoryRole);
0047     setData(license(), PlasmaAppletItemModel::LicenseRole);
0048     setData(website(), PlasmaAppletItemModel::WebsiteRole);
0049     setData(version(), PlasmaAppletItemModel::VersionRole);
0050     setData(author(), PlasmaAppletItemModel::AuthorRole);
0051     setData(email(), PlasmaAppletItemModel::EmailRole);
0052     setData(apiVersion(), PlasmaAppletItemModel::ApiVersionRole);
0053     setData(isSupported(), PlasmaAppletItemModel::IsSupportedRole);
0054     setData(unsupportedMessage(), PlasmaAppletItemModel::UnsupportedMessageRole);
0055     setData(0, PlasmaAppletItemModel::RunningRole);
0056     setData(m_local, PlasmaAppletItemModel::LocalRole);
0057 }
0058 
0059 QString PlasmaAppletItem::pluginName() const
0060 {
0061     return m_info.pluginId();
0062 }
0063 
0064 QString PlasmaAppletItem::name() const
0065 {
0066     return m_info.name();
0067 }
0068 
0069 QString PlasmaAppletItem::description() const
0070 {
0071     return m_info.description();
0072 }
0073 
0074 QString PlasmaAppletItem::license() const
0075 {
0076     return m_info.license();
0077 }
0078 
0079 QString PlasmaAppletItem::category() const
0080 {
0081     return m_info.category();
0082 }
0083 
0084 QString PlasmaAppletItem::website() const
0085 {
0086     return m_info.website();
0087 }
0088 
0089 QString PlasmaAppletItem::version() const
0090 {
0091     return m_info.version();
0092 }
0093 
0094 QString PlasmaAppletItem::author() const
0095 {
0096     if (m_info.authors().isEmpty()) {
0097         return QString();
0098     }
0099 
0100     return m_info.authors().constFirst().name();
0101 }
0102 
0103 QString PlasmaAppletItem::email() const
0104 {
0105     if (m_info.authors().isEmpty()) {
0106         return QString();
0107     }
0108 
0109     return m_info.authors().constFirst().emailAddress();
0110 }
0111 
0112 int PlasmaAppletItem::running() const
0113 {
0114     return m_runningCount;
0115 }
0116 
0117 void PlasmaAppletItem::setRunning(int count)
0118 {
0119     m_runningCount = count;
0120     setData(count, PlasmaAppletItemModel::RunningRole);
0121     emitDataChanged();
0122 }
0123 
0124 QString PlasmaAppletItem::apiVersion() const
0125 {
0126     return m_info.value(QStringLiteral("X-Plasma-API-Minimum-Version"));
0127 }
0128 
0129 bool PlasmaAppletItem::isSupported() const
0130 {
0131     QVersionNumber version = QVersionNumber::fromString(apiVersion());
0132     if (version.majorVersion() != 6 /*PROJECT_VERSION_MAJOR*/) {
0133         return false;
0134     } else if (version.minorVersion() > 6 /*PROJECT_VERSION_MINOR*/) {
0135         return false;
0136     }
0137     return true;
0138 }
0139 
0140 QString PlasmaAppletItem::unsupportedMessage() const
0141 {
0142     const QString versionString = apiVersion();
0143     QVersionNumber version = QVersionNumber::fromString(versionString);
0144 
0145     if (version.isNull()) {
0146         // TODO: We have to hardcode 6 for now as PROJECT_VERSION_MAJOR is still 5, change it back to PROJECT_VERSION_MAJOR with 6.0
0147         return i18n(
0148             "This Widget was written for an unknown older version of Plasma and is not compatible with Plasma %1. Please contact the widget's author for an "
0149             "updated version.",
0150             6 /*PROJECT_VERSION_MAJOR*/);
0151     } else if (version.majorVersion() < 6 /*PROJECT_VERSION_MAJOR*/) {
0152         return i18n("This Widget was written for Plasma %1 and is not compatible with Plasma %2. Please contact the widget's author for an updated version.",
0153                     version.majorVersion(),
0154                     6 /*PROJECT_VERSION_MAJOR*/);
0155     } else if (version.majorVersion() > 6 /*PROJECT_VERSION_MAJOR*/) {
0156         return i18n("This Widget was written for Plasma %1 and is not compatible with Plasma %2. Please update Plasma in order to use the widget.",
0157                     version.majorVersion(),
0158                     6 /*PROJECT_VERSION_MAJOR*/);
0159     } else if (version.minorVersion() > PROJECT_VERSION_MINOR) {
0160         return i18n(
0161             "This Widget was written for Plasma %1 and is not compatible with the latest version of Plasma. Please update Plasma in order to use the widget.",
0162             versionString);
0163     }
0164 
0165     return QString();
0166 }
0167 
0168 static bool matchesKeywords(QStringView keywords, const QString &pattern)
0169 {
0170     const auto l = keywords.split(QLatin1Char(';'), Qt::SkipEmptyParts);
0171     for (const auto &keyword : l) {
0172         if (keyword.startsWith(pattern, Qt::CaseInsensitive)) {
0173             return true;
0174         }
0175     }
0176     return false;
0177 }
0178 
0179 bool PlasmaAppletItem::matches(const QString &pattern) const
0180 {
0181     const QJsonObject rawData = m_info.rawData();
0182     if (matchesKeywords(KJsonUtils::readTranslatedString(rawData, QStringLiteral("Keywords")), pattern)) {
0183         return true;
0184     }
0185 
0186     // Add English name and keywords so users in other languages won't have to switch IME when searching.
0187     if (!QLocale().name().startsWith(QLatin1String("en_"))) {
0188         const QString name(rawData[QStringLiteral("KPlugin")][QStringLiteral("Name")].toString());
0189         const QString keywords(rawData[QStringLiteral("KPlugin")][QStringLiteral("Name")].toString());
0190         if (name.startsWith(pattern, Qt::CaseInsensitive) || matchesKeywords(keywords, pattern)) {
0191             return true;
0192         }
0193     }
0194 
0195     return AbstractItem::matches(pattern);
0196 }
0197 
0198 QStringList PlasmaAppletItem::keywords() const
0199 {
0200     const static QString keywordsJsonKey = QStringLiteral("X-KDE-Keywords");
0201     constexpr QLatin1Char separator(',');
0202 
0203     const QJsonObject rawData = m_info.rawData();
0204     if (rawData.contains(keywordsJsonKey)) {
0205         QStringList keywords = m_info.value(keywordsJsonKey).split(separator);
0206         keywords << KJsonUtils::readTranslatedString(rawData, keywordsJsonKey).split(separator);
0207         keywords.removeDuplicates();
0208         return keywords;
0209     }
0210     return {};
0211 }
0212 
0213 bool PlasmaAppletItem::isLocal() const
0214 {
0215     return m_local;
0216 }
0217 
0218 bool PlasmaAppletItem::passesFiltering(const KCategorizedItemsViewModels::Filter &filter) const
0219 {
0220     if (filter.first == QLatin1String("running")) {
0221         return running();
0222     } else if (filter.first == QLatin1String("local")) {
0223         return isLocal();
0224     } else if (filter.first == QLatin1String("category")) {
0225         return m_info.category().toLower() == filter.second;
0226     } else {
0227         return false;
0228     }
0229 }
0230 
0231 QMimeData *PlasmaAppletItem::mimeData() const
0232 {
0233     QMimeData *data = new QMimeData();
0234     QByteArray appletName;
0235     appletName += pluginName().toUtf8();
0236     data->setData(mimeTypes().at(0), appletName);
0237     return data;
0238 }
0239 
0240 QStringList PlasmaAppletItem::mimeTypes() const
0241 {
0242     QStringList types;
0243     types << QStringLiteral("text/x-plasmoidservicename");
0244     return types;
0245 }
0246 
0247 QVariant PlasmaAppletItem::data(int role) const
0248 {
0249     switch (role) {
0250     case PlasmaAppletItemModel::ScreenshotRole:
0251         // null = not yet done, empty = tried and failed
0252         if (m_screenshot.isNull()) {
0253             KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"));
0254             pkg.setDefaultPackageRoot(QStringLiteral("plasma/plasmoids"));
0255             pkg.setPath(m_info.pluginId());
0256             if (pkg.isValid()) {
0257                 const_cast<PlasmaAppletItem *>(this)->m_screenshot = pkg.filePath("screenshot");
0258             } else {
0259                 const_cast<PlasmaAppletItem *>(this)->m_screenshot = QString();
0260             }
0261         } else if (m_screenshot.isEmpty()) {
0262             return QVariant();
0263         }
0264         return m_screenshot;
0265 
0266     case Qt::DecorationRole: {
0267         // null = not yet done, empty = tried and failed
0268         if (m_icon.isNull()) {
0269             KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"));
0270             pkg.setDefaultPackageRoot(QStringLiteral("plasma/plasmoids"));
0271             pkg.setPath(m_info.pluginId());
0272             if (pkg.isValid() && pkg.metadata().iconName().startsWith(QLatin1String("/"))) {
0273                 const_cast<PlasmaAppletItem *>(this)->m_icon = pkg.filePath("", pkg.metadata().iconName().toUtf8());
0274             } else {
0275                 const_cast<PlasmaAppletItem *>(this)->m_icon = QString();
0276                 return AbstractItem::data(role);
0277             }
0278         }
0279         if (m_icon.isEmpty()) {
0280             return AbstractItem::data(role);
0281         }
0282         return QIcon(m_icon);
0283     }
0284 
0285     default:
0286         return AbstractItem::data(role);
0287     }
0288 }
0289 
0290 // PlasmaAppletItemModel
0291 
0292 PlasmaAppletItemModel::PlasmaAppletItemModel(QObject *parent)
0293     : QStandardItemModel(parent)
0294     , m_startupCompleted(false)
0295 {
0296     setSortRole(Qt::DisplayRole);
0297 }
0298 
0299 QHash<int, QByteArray> PlasmaAppletItemModel::roleNames() const
0300 {
0301     QHash<int, QByteArray> newRoleNames = QAbstractItemModel::roleNames();
0302     newRoleNames[NameRole] = "name";
0303     newRoleNames[PluginNameRole] = "pluginName";
0304     newRoleNames[DescriptionRole] = "description";
0305     newRoleNames[CategoryRole] = "category";
0306     newRoleNames[LicenseRole] = "license";
0307     newRoleNames[WebsiteRole] = "website";
0308     newRoleNames[VersionRole] = "version";
0309     newRoleNames[AuthorRole] = "author";
0310     newRoleNames[EmailRole] = "email";
0311     newRoleNames[RunningRole] = "running";
0312     newRoleNames[LocalRole] = "local";
0313     newRoleNames[ScreenshotRole] = "screenshot";
0314     newRoleNames[ApiVersionRole] = "apiVersion";
0315     newRoleNames[IsSupportedRole] = "isSupported";
0316     newRoleNames[UnsupportedMessageRole] = "unsupportedMessage";
0317     return newRoleNames;
0318 }
0319 
0320 void PlasmaAppletItemModel::populateModel()
0321 {
0322     clear();
0323 
0324     auto filter = [this](const KPluginMetaData &plugin) -> bool {
0325         const QStringList provides = plugin.value(QStringLiteral("X-Plasma-Provides"), QStringList());
0326 
0327         if (!m_provides.isEmpty()) {
0328             const bool providesFulfilled = std::any_of(m_provides.cbegin(), m_provides.cend(), [&provides](const QString &p) {
0329                 return provides.contains(p);
0330             });
0331 
0332             if (!providesFulfilled) {
0333                 return false;
0334             }
0335         }
0336 
0337         if (!plugin.isValid() || plugin.rawData().value(QStringLiteral("NoDisplay")).toBool() || plugin.category() == QLatin1String("Containments")) {
0338             // we don't want to show the hidden category
0339             return false;
0340         }
0341 
0342         static const auto formFactors = KRuntimePlatform::runtimePlatform();
0343         // If runtimePlatformis not defined, accept everything
0344         bool inFormFactor = formFactors.isEmpty();
0345 
0346         for (const QString &formFactor : formFactors) {
0347             if (plugin.formFactors().isEmpty() || plugin.formFactors().contains(formFactor)) {
0348                 inFormFactor = true;
0349                 break;
0350             }
0351         }
0352 
0353         if (!inFormFactor) {
0354             return false;
0355         }
0356 
0357         return true;
0358     };
0359 
0360     QList<KPluginMetaData> packages =
0361         KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/Applet"), QStringLiteral("plasma/plasmoids"), filter);
0362 
0363     // NOTE: Those 2 extra searches are for pure retrocompatibility, to list old plasmoids
0364     // Just to give the user the possibility to remove them.
0365     // Eventually after a year or two, this code can be removed to drop this transition support
0366 
0367     // Search all of those that have a desktop file metadata, those are plasma 5 plasmoids
0368     QList<KPackage::Package> kPackages = KPackage::PackageLoader::self()->listKPackages(QStringLiteral("Plasma/Applet"), QStringLiteral("plasma/plasmoids"));
0369     for (const KPackage::Package &package : kPackages) {
0370         KPluginMetaData data = package.metadata();
0371         if (package.filePath("metadata").endsWith(QStringLiteral("metadata.desktop")) && filter(data)) {
0372             appendRow(new PlasmaAppletItem(data));
0373         }
0374     }
0375     // Search all packages that have json metadata but not a correct Plasma/Applet and put them at the end: assume they are plasma5 plasmoids
0376     packages.append(
0377         KPackage::PackageLoader::self()->findPackages(QString(), QStringLiteral("plasma/plasmoids"), [&filter](const KPluginMetaData &plugin) -> bool {
0378             return plugin.value(QStringLiteral("KPackageStructure")) != QStringLiteral("Plasma/Applet") && filter(plugin);
0379         }));
0380 
0381     for (const KPluginMetaData &plugin : packages) {
0382         appendRow(new PlasmaAppletItem(plugin));
0383     }
0384 
0385     Q_EMIT modelPopulated();
0386 }
0387 
0388 void PlasmaAppletItemModel::setRunningApplets(const QHash<QString, int> &apps)
0389 {
0390     // for each item, find that string and set the count
0391     for (int r = 0; r < rowCount(); ++r) {
0392         QStandardItem *i = item(r);
0393         PlasmaAppletItem *p = dynamic_cast<PlasmaAppletItem *>(i);
0394 
0395         if (p) {
0396             const int running = apps.value(p->pluginName());
0397             p->setRunning(running);
0398         }
0399     }
0400 }
0401 
0402 void PlasmaAppletItemModel::setRunningApplets(const QString &name, int count)
0403 {
0404     for (int r = 0; r < rowCount(); ++r) {
0405         QStandardItem *i = item(r);
0406         PlasmaAppletItem *p = dynamic_cast<PlasmaAppletItem *>(i);
0407         if (p && p->pluginName() == name) {
0408             p->setRunning(count);
0409         }
0410     }
0411 }
0412 
0413 QStringList PlasmaAppletItemModel::mimeTypes() const
0414 {
0415     QStringList types;
0416     types << QStringLiteral("text/x-plasmoidservicename");
0417     return types;
0418 }
0419 
0420 QMimeData *PlasmaAppletItemModel::mimeData(const QModelIndexList &indexes) const
0421 {
0422     if (indexes.count() <= 0) {
0423         return nullptr;
0424     }
0425 
0426     QStringList types = mimeTypes();
0427 
0428     if (types.isEmpty()) {
0429         return nullptr;
0430     }
0431 
0432     QMimeData *data = new QMimeData();
0433 
0434     QString format = types.at(0);
0435 
0436     QByteArray appletNames;
0437     int lastRow = -1;
0438     for (const QModelIndex &index : indexes) {
0439         if (index.row() == lastRow) {
0440             continue;
0441         }
0442 
0443         lastRow = index.row();
0444         PlasmaAppletItem *selectedItem = (PlasmaAppletItem *)itemFromIndex(index);
0445         appletNames += '\n' + selectedItem->pluginName().toUtf8();
0446         // qDebug() << selectedItem->pluginName() << index.column() << index.row();
0447     }
0448 
0449     data->setData(format, appletNames);
0450     return data;
0451 }
0452 
0453 QStringList PlasmaAppletItemModel::provides() const
0454 {
0455     return m_provides;
0456 }
0457 
0458 void PlasmaAppletItemModel::setProvides(const QStringList &provides)
0459 {
0460     if (m_provides == provides) {
0461         return;
0462     }
0463 
0464     m_provides = provides;
0465     if (m_startupCompleted) {
0466         populateModel();
0467     }
0468 }
0469 
0470 void PlasmaAppletItemModel::setApplication(const QString &app)
0471 {
0472     m_application = app;
0473     if (m_startupCompleted) {
0474         populateModel();
0475     }
0476 }
0477 
0478 bool PlasmaAppletItemModel::startupCompleted() const
0479 {
0480     return m_startupCompleted;
0481 }
0482 
0483 void PlasmaAppletItemModel::setStartupCompleted(bool complete)
0484 {
0485     m_startupCompleted = complete;
0486 }
0487 
0488 QString &PlasmaAppletItemModel::Application()
0489 {
0490     return m_application;
0491 }
0492 
0493 // #include <plasmaappletitemmodel_p.moc>