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