File indexing completed on 2024-05-12 05:38:17
0001 /* 0002 SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez <aleixpol@kde.org> 0003 0004 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 #include "appstreamrunner.h" 0008 0009 #include <unordered_set> 0010 0011 #include <AppStreamQt/icon.h> 0012 0013 #include <QDebug> 0014 #include <QDesktopServices> 0015 #include <QDir> 0016 #include <QIcon> 0017 #include <QTimer> 0018 0019 #include <KApplicationTrader> 0020 #include <KIO/OpenUrlJob> 0021 #include <KLocalizedString> 0022 #include <KNotificationJobUiDelegate> 0023 #include <KSycoca> 0024 0025 #include "debug.h" 0026 0027 K_PLUGIN_CLASS_WITH_JSON(InstallerRunner, "plasma-runner-appstream.json") 0028 0029 InstallerRunner::InstallerRunner(QObject *parent, const KPluginMetaData &metaData) 0030 : KRunner::AbstractRunner(parent, metaData) 0031 { 0032 addSyntax(":q:", i18n("Looks for non-installed components according to :q:")); 0033 setMinLetterCount(3); 0034 } 0035 0036 static QIcon componentIcon(const AppStream::Component &comp) 0037 { 0038 QIcon ret; 0039 const auto icons = comp.icons(); 0040 if (icons.isEmpty()) { 0041 ret = QIcon::fromTheme(QStringLiteral("package-x-generic")); 0042 } else 0043 for (const AppStream::Icon &icon : icons) { 0044 QStringList stock; 0045 switch (icon.kind()) { 0046 case AppStream::Icon::KindLocal: 0047 ret.addFile(icon.url().toLocalFile(), icon.size()); 0048 break; 0049 case AppStream::Icon::KindCached: 0050 ret.addFile(icon.url().toLocalFile(), icon.size()); 0051 break; 0052 case AppStream::Icon::KindStock: 0053 stock += icon.name(); 0054 break; 0055 default: 0056 break; 0057 } 0058 if (ret.isNull() && !stock.isEmpty()) { 0059 ret = QIcon::fromTheme(stock.first()); 0060 } 0061 } 0062 return ret; 0063 } 0064 0065 void InstallerRunner::match(KRunner::RunnerContext &context) 0066 { 0067 // Give the other runners a bit of time to produce results 0068 QEventLoop loop; 0069 QTimer::singleShot(200, &loop, &QEventLoop::quit); 0070 loop.exec(); 0071 if (!context.isValid()) { 0072 return; 0073 } 0074 0075 // Check if other plugins have already found an executable, if that is the case we do 0076 // not want to ask the user to install anything else 0077 const QList<KRunner::QueryMatch> matches = context.matches(); 0078 const bool execFound = std::any_of(matches.cbegin(), matches.cend(), [](const KRunner::QueryMatch &match) { 0079 return match.id().startsWith(QLatin1String("exec://")); 0080 }); 0081 if (execFound) { 0082 return; 0083 } 0084 0085 std::unordered_set<QString> uniqueIds; 0086 const auto components = findComponentsByString(context.query()); 0087 0088 for (auto it = components.cbegin(); it != components.cend() && uniqueIds.size() < 3; it = std::next(it)) { 0089 if (it->kind() != AppStream::Component::KindDesktopApp) 0090 continue; 0091 0092 const QString componentId = it->id(); 0093 const auto servicesFound = KApplicationTrader::query([&componentId](const KService::Ptr &service) { 0094 if (service->exec().isEmpty()) 0095 return false; 0096 0097 if (service->desktopEntryName().compare(componentId, Qt::CaseInsensitive) == 0) 0098 return true; 0099 0100 const auto idWithoutDesktop = QString(componentId).remove(".desktop"); 0101 if (service->desktopEntryName().compare(idWithoutDesktop, Qt::CaseInsensitive) == 0) 0102 return true; 0103 0104 const auto renamedFrom = service->property<QStringList>("X-Flatpak-RenamedFrom"); 0105 if (renamedFrom.contains(componentId, Qt::CaseInsensitive) || renamedFrom.contains(idWithoutDesktop, Qt::CaseInsensitive)) 0106 return true; 0107 0108 return false; 0109 }); 0110 0111 if (!servicesFound.isEmpty()) 0112 continue; 0113 const auto [_, inserted] = uniqueIds.insert(componentId); 0114 if (!inserted) { 0115 continue; 0116 } 0117 0118 KRunner::QueryMatch match(this); 0119 match.setCategoryRelevance(KRunner::QueryMatch::CategoryRelevance::Lowest); // Make sure it is less relavant than KCMs or apps 0120 match.setId(componentId); 0121 match.setIcon(componentIcon(*it)); 0122 match.setText(i18n("Get %1…", it->name())); 0123 match.setSubtext(it->summary()); 0124 match.setData(QUrl("appstream://" + componentId)); 0125 match.setRelevance(it->name().compare(context.query(), Qt::CaseInsensitive) == 0 ? 1. : 0.7); 0126 context.addMatch(match); 0127 } 0128 } 0129 0130 void InstallerRunner::run(const KRunner::RunnerContext & /*context*/, const KRunner::QueryMatch &match) 0131 { 0132 const QUrl appstreamUrl = match.data().toUrl(); 0133 0134 auto job = new KIO::OpenUrlJob(appstreamUrl); 0135 job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); 0136 job->start(); 0137 } 0138 0139 void InstallerRunner::init() 0140 { 0141 // KApplicationTrader uses KService which uses KSycoca which holds 0142 // KDirWatch instances to monitor changes. We don't need this on 0143 // our runner threads - let's not needlessly allocate inotify instances. 0144 KSycoca::disableAutoRebuild(); 0145 } 0146 0147 QList<AppStream::Component> InstallerRunner::findComponentsByString(const QString &query) 0148 { 0149 static bool warnedOnce = false; 0150 static bool opened = m_db.load(); 0151 if (!opened) { 0152 if (warnedOnce) { 0153 qCDebug(RUNNER_APPSTREAM) << "Had errors when loading AppStream metadata pool" << m_db.lastError(); 0154 } else { 0155 qCWarning(RUNNER_APPSTREAM) << "Had errors when loading AppStream metadata pool" << m_db.lastError(); 0156 warnedOnce = true; 0157 } 0158 } 0159 0160 return m_db.search(query).toList(); 0161 } 0162 0163 #include "appstreamrunner.moc"