File indexing completed on 2024-04-28 05:36:49
0001 /* 0002 * SPDX-FileCopyrightText: 2017-2019 Red Hat Inc 0003 * SPDX-FileCopyrightText: 2020-2022 Harald Sitter <sitter@kde.org> 0004 * 0005 * SPDX-License-Identifier: LGPL-2.0-or-later 0006 * 0007 * SPDX-FileCopyrightText: 2017-2019 Jan Grulich <jgrulich@redhat.com> 0008 */ 0009 0010 #include "appchooserdialog.h" 0011 #include "appchooser_debug.h" 0012 0013 #include <algorithm> 0014 #include <iterator> 0015 0016 #include <QQmlContext> 0017 #include <QQmlEngine> 0018 #include <QQuickItem> 0019 #include <QQuickWidget> 0020 0021 #include <QApplication> 0022 #include <QDir> 0023 #include <QMimeDatabase> 0024 #include <QSettings> 0025 #include <QStandardPaths> 0026 0027 #include <KApplicationTrader> 0028 #include <KBuildSycocaProgressDialog> 0029 #include <KIO/MimeTypeFinderJob> 0030 #include <KLocalizedString> 0031 #include <KProcess> 0032 0033 AppChooserDialog::AppChooserDialog(const QStringList &choices, const QString &lastUsedApp, const QString &fileName, const QString &mimeName, QObject *parent) 0034 : QuickDialog(parent) 0035 , m_model(new AppModel(this)) 0036 , m_appChooserData(new AppChooserData(this)) 0037 { 0038 QVariantMap props = { 0039 {"title", i18nc("@title:window", "Choose Application")}, 0040 // fileName is actually the full path, confusingly enough. But showing the 0041 // whole thing is overkill; let's just show the user the file itself 0042 {"mainText", xi18nc("@info", "Choose an application to open <filename>%1</filename>", QUrl::fromLocalFile(fileName).fileName())}, 0043 }; 0044 0045 auto filterModel = new AppFilterModel(this); 0046 filterModel->setSourceModel(m_model); 0047 0048 m_appChooserData->setFileName(fileName); 0049 m_appChooserData->setLastUsedApp(lastUsedApp); 0050 filterModel->setLastUsedApp(lastUsedApp); 0051 0052 auto findDefaultApp = [this, filterModel]() { 0053 KService::Ptr defaultService = KApplicationTrader::preferredService(m_appChooserData->mimeName()); 0054 if (defaultService && defaultService->isValid()) { 0055 QString id = defaultService->desktopEntryName(); 0056 m_appChooserData->setDefaultApp(id); 0057 filterModel->setDefaultApp(id); 0058 } 0059 }; 0060 0061 auto findPreferredApps = [this, choices]() { 0062 if (!choices.isEmpty()) { 0063 m_model->setPreferredApps(choices); 0064 return; 0065 } 0066 QStringList choices; 0067 const KService::List appServices = KApplicationTrader::queryByMimeType(m_appChooserData->mimeName(), [](const KService::Ptr &service) -> bool { 0068 return service->isValid(); 0069 }); 0070 std::transform(appServices.begin(), appServices.end(), std::back_inserter(choices), [](const KService::Ptr &service) { 0071 return service ? service->desktopEntryName() : QString(); 0072 }); 0073 m_model->setPreferredApps(choices); 0074 }; 0075 0076 if (mimeName.isEmpty()) { 0077 auto job = new KIO::MimeTypeFinderJob(QUrl::fromUserInput(fileName)); 0078 job->setAuthenticationPromptEnabled(false); 0079 connect(job, &KIO::MimeTypeFinderJob::result, this, [this, job, findDefaultApp, findPreferredApps]() { 0080 if (job->error() == KJob::NoError) { 0081 m_appChooserData->setMimeName(job->mimeType()); 0082 findDefaultApp(); 0083 findPreferredApps(); 0084 } else { 0085 qCWarning(XdgDesktopPortalKdeAppChooser) << "couldn't get mimetype:" << job->errorString(); 0086 } 0087 }); 0088 job->start(); 0089 } else { 0090 m_appChooserData->setMimeName(mimeName); 0091 findDefaultApp(); 0092 findPreferredApps(); 0093 } 0094 0095 qmlRegisterSingletonInstance<AppFilterModel>("org.kde.xdgdesktopportal", 1, 0, "AppModel", filterModel); 0096 qmlRegisterSingletonInstance<AppChooserData>("org.kde.xdgdesktopportal", 1, 0, "AppChooserData", m_appChooserData); 0097 0098 create(QStringLiteral("qrc:/AppChooserDialog.qml"), props); 0099 0100 connect(m_appChooserData, &AppChooserData::openDiscover, this, &AppChooserDialog::onOpenDiscover); 0101 connect(m_appChooserData, &AppChooserData::applicationSelected, this, &AppChooserDialog::onApplicationSelected); 0102 } 0103 0104 QString AppChooserDialog::selectedApplication() const 0105 { 0106 return m_selectedApplication; 0107 } 0108 0109 void AppChooserDialog::onApplicationSelected(const QString &desktopFile, const bool remember) 0110 { 0111 m_selectedApplication = desktopFile; 0112 0113 if (remember && !m_appChooserData->mimeName().isEmpty()) { 0114 KService::Ptr serv = KService::serviceByDesktopName(desktopFile); 0115 KApplicationTrader::setPreferredService(m_appChooserData->mimeName(), serv); 0116 // kbuildsycoca is the one reading mimeapps.list, so we need to run it now 0117 KBuildSycocaProgressDialog::rebuildKSycoca(QApplication::activeWindow()); 0118 } 0119 0120 accept(); 0121 } 0122 0123 void AppChooserDialog::onOpenDiscover() 0124 { 0125 QStringList args; 0126 if (!m_appChooserData->mimeName().isEmpty()) { 0127 args << QStringLiteral("--mime") << m_appChooserData->mimeName(); 0128 } 0129 KProcess::startDetached(QStringLiteral("plasma-discover"), args); 0130 } 0131 0132 void AppChooserDialog::updateChoices(const QStringList &choices) 0133 { 0134 m_model->setPreferredApps(choices); 0135 } 0136 0137 ApplicationItem::ApplicationItem(const QString &name, const KService::Ptr &service) 0138 : m_applicationName(name) 0139 , m_applicationService(service) 0140 , m_applicationCategory(AllApplications) 0141 { 0142 const QStringList names = service->mimeTypes(); 0143 const QMimeDatabase database; 0144 for (const QString &name : names) { 0145 QMimeType mime = database.mimeTypeForName(name); 0146 if (mime.isValid()) { 0147 m_supportedMimeTypes.append(mime); 0148 } 0149 } 0150 } 0151 0152 QString ApplicationItem::applicationName() const 0153 { 0154 return m_applicationName; 0155 } 0156 0157 QString ApplicationItem::applicationGenericName() const 0158 { 0159 return m_applicationService->genericName(); 0160 } 0161 0162 QString ApplicationItem::applicationUntranslatedGenericName() const 0163 { 0164 return m_applicationService->untranslatedGenericName(); 0165 } 0166 0167 QString ApplicationItem::applicationIcon() const 0168 { 0169 return m_applicationService->icon(); 0170 } 0171 0172 QString ApplicationItem::applicationDesktopFile() const 0173 { 0174 return m_applicationService->desktopEntryName(); 0175 } 0176 0177 QList<QMimeType> ApplicationItem::supportedMimeTypes() const 0178 { 0179 return m_supportedMimeTypes; 0180 } 0181 0182 void ApplicationItem::setApplicationCategory(ApplicationItem::ApplicationCategory category) 0183 { 0184 m_applicationCategory = category; 0185 } 0186 0187 ApplicationItem::ApplicationCategory ApplicationItem::applicationCategory() const 0188 { 0189 return m_applicationCategory; 0190 } 0191 0192 bool ApplicationItem::operator==(const ApplicationItem &item) const 0193 { 0194 return item.applicationDesktopFile() == applicationDesktopFile(); 0195 } 0196 0197 AppFilterModel::AppFilterModel(QObject *parent) 0198 : QSortFilterProxyModel(parent) 0199 { 0200 setDynamicSortFilter(true); 0201 setFilterCaseSensitivity(Qt::CaseInsensitive); 0202 sort(0, Qt::DescendingOrder); 0203 } 0204 0205 void AppFilterModel::setShowOnlyPreferredApps(bool show) 0206 { 0207 if (m_showOnlyPreferredApps == show) { 0208 return; 0209 } 0210 0211 m_showOnlyPreferredApps = show; 0212 Q_EMIT showOnlyPreferredAppsChanged(); 0213 invalidate(); 0214 } 0215 0216 bool AppFilterModel::showOnlyPreferredApps() const 0217 { 0218 return m_showOnlyPreferredApps; 0219 } 0220 0221 void AppFilterModel::setDefaultApp(const QString &defaultApp) 0222 { 0223 m_defaultApp = defaultApp; 0224 0225 invalidate(); 0226 } 0227 0228 QString AppFilterModel::defaultApp() const 0229 { 0230 return m_defaultApp; 0231 } 0232 0233 void AppFilterModel::setLastUsedApp(const QString &lastUsedApp) 0234 { 0235 if (m_lastUsedApp == lastUsedApp) { 0236 return; 0237 } 0238 0239 m_lastUsedApp = lastUsedApp; 0240 invalidate(); 0241 } 0242 0243 QString AppFilterModel::lastUsedApp() const 0244 { 0245 return m_lastUsedApp; 0246 } 0247 0248 void AppFilterModel::setFilter(const QString &text) 0249 { 0250 m_filter = text; 0251 0252 if (!m_filter.isEmpty() && m_showOnlyPreferredApps) { 0253 m_showOnlyPreferredApps = false; 0254 emit showOnlyPreferredAppsChanged(); 0255 } 0256 0257 invalidate(); 0258 } 0259 0260 QString AppFilterModel::filter() const 0261 { 0262 return m_filter; 0263 } 0264 0265 bool AppFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const 0266 { 0267 const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); 0268 0269 ApplicationItem::ApplicationCategory category = 0270 static_cast<ApplicationItem::ApplicationCategory>(sourceModel()->data(index, AppModel::ApplicationCategoryRole).toInt()); 0271 0272 const bool canShowInList = m_showOnlyPreferredApps ? (category == ApplicationItem::PreferredApplication) : true; 0273 if (!canShowInList) { 0274 return false; 0275 } 0276 0277 if (m_filter.isEmpty()) { 0278 return true; 0279 } 0280 0281 const QString appName = index.data(AppModel::ApplicationNameRole).toString(); 0282 if (appName.contains(m_filter, Qt::CaseInsensitive)) { 0283 return true; 0284 } 0285 0286 // Match in GenericName 0287 const QString genericName = index.data(AppModel::ApplicationGenericNameRole).toString(); 0288 if (genericName.contains(m_filter, Qt::CaseInsensitive)) { 0289 return true; 0290 } 0291 0292 // Match in untranslated GenericName 0293 const QString untranslatedGenericName = index.data(AppModel::ApplicationUntranslatedGenericNameRole).toString(); 0294 if (untranslatedGenericName.contains(m_filter, Qt::CaseInsensitive)) { 0295 return true; 0296 } 0297 0298 // Match in MimeTypes 0299 const auto supportedMimeTypes = index.data(AppModel::ApplicationSupportedMimeTypesRole).value<QList<QMimeType>>(); 0300 return std::any_of(supportedMimeTypes.cbegin(), supportedMimeTypes.cend(), [this, category](const QMimeType &type) { 0301 if (type.name().contains(m_filter, Qt::CaseInsensitive)) { 0302 return true; 0303 } 0304 0305 const QStringList aliases = type.aliases(); 0306 const bool aliasesMatched = std::any_of(aliases.cbegin(), aliases.cend(), [this](const QString &name) { 0307 return name.contains(m_filter, Qt::CaseInsensitive); 0308 }); 0309 if (aliasesMatched) { 0310 return true; 0311 } 0312 0313 const QStringList suffixes = type.suffixes(); 0314 if (suffixes.contains(m_filter) || suffixes.contains(m_filter.mid(m_filter.lastIndexOf(QLatin1Char('.')) + 1))) { 0315 return true; 0316 } 0317 0318 return false; 0319 }); 0320 } 0321 0322 bool AppFilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const 0323 { 0324 if (sourceModel()->data(left, AppModel::ApplicationDesktopFileRole) == m_defaultApp) { 0325 return false; 0326 } 0327 if (sourceModel()->data(right, AppModel::ApplicationDesktopFileRole) == m_defaultApp) { 0328 return true; 0329 } 0330 0331 const QString leftName = left.data(AppModel::ApplicationNameRole).toString(); 0332 const QString rightName = right.data(AppModel::ApplicationNameRole).toString(); 0333 0334 // Prioritize name match when filter is not empty 0335 if (!m_filter.isEmpty()) { 0336 const bool leftMatched = leftName.contains(m_filter, Qt::CaseInsensitive); 0337 const bool rightMatched = rightName.contains(m_filter, Qt::CaseInsensitive); 0338 if (leftMatched && !rightMatched) { 0339 return false; 0340 } else if (!leftMatched && rightMatched) { 0341 return true; 0342 } 0343 } 0344 0345 if (sourceModel()->data(left, AppModel::ApplicationDesktopFileRole) == m_lastUsedApp) { 0346 return false; 0347 } 0348 if (sourceModel()->data(right, AppModel::ApplicationDesktopFileRole) == m_lastUsedApp) { 0349 return true; 0350 } 0351 ApplicationItem::ApplicationCategory leftCategory = 0352 static_cast<ApplicationItem::ApplicationCategory>(sourceModel()->data(left, AppModel::ApplicationCategoryRole).toInt()); 0353 ApplicationItem::ApplicationCategory rightCategory = 0354 static_cast<ApplicationItem::ApplicationCategory>(sourceModel()->data(right, AppModel::ApplicationCategoryRole).toInt()); 0355 0356 if (int comp = leftCategory - rightCategory; comp != 0) { 0357 return comp > 0; 0358 } 0359 0360 return QString::localeAwareCompare(leftName, rightName) > 0; 0361 } 0362 0363 QString AppChooserData::defaultApp() const 0364 { 0365 return m_defaultApp; 0366 } 0367 0368 void AppChooserData::setDefaultApp(const QString &defaultApp) 0369 { 0370 m_defaultApp = defaultApp; 0371 Q_EMIT defaultAppChanged(); 0372 } 0373 0374 QString AppChooserData::lastUsedApp() const 0375 { 0376 return m_lastUsedApp; 0377 } 0378 0379 void AppChooserData::setLastUsedApp(const QString &lastUsedApp) 0380 { 0381 if (m_lastUsedApp == lastUsedApp) { 0382 return; 0383 } 0384 0385 m_lastUsedApp = lastUsedApp; 0386 Q_EMIT lastUsedAppChanged(); 0387 } 0388 0389 AppChooserData::AppChooserData(QObject *parent) 0390 : QObject(parent) 0391 { 0392 } 0393 0394 QString AppChooserData::fileName() const 0395 { 0396 return m_fileName; 0397 } 0398 0399 void AppChooserData::setFileName(const QString &fileName) 0400 { 0401 m_fileName = fileName; 0402 Q_EMIT fileNameChanged(); 0403 } 0404 0405 QString AppChooserData::mimeName() const 0406 { 0407 return m_mimeName; 0408 } 0409 0410 void AppChooserData::setMimeName(const QString &mimeName) 0411 { 0412 if (m_mimeName != mimeName) { 0413 m_mimeName = mimeName; 0414 Q_EMIT mimeNameChanged(); 0415 Q_EMIT mimeDescChanged(); 0416 } 0417 } 0418 0419 QString AppChooserData::mimeDesc() const 0420 { 0421 return QMimeDatabase().mimeTypeForName(m_mimeName).comment(); 0422 } 0423 0424 AppModel::AppModel(QObject *parent) 0425 : QAbstractListModel(parent) 0426 { 0427 loadApplications(); 0428 } 0429 0430 void AppModel::setPreferredApps(const QStringList &possiblyAliasedList) 0431 { 0432 m_hasPreferredApps = false; 0433 Q_EMIT hasPreferredAppsChanged(); 0434 0435 // In the event that we get incoming NoDisplay entries that are AliasFor another desktop file, 0436 // switch the NoDisplay name for the aliased name. 0437 QStringList list; 0438 for (const auto &entry : possiblyAliasedList) { 0439 if (const auto value = m_noDisplayAliasesFor.value(entry); !value.isEmpty()) { 0440 list << value; 0441 } else { 0442 list << entry; 0443 } 0444 } 0445 0446 for (ApplicationItem &item : m_list) { 0447 bool changed = false; 0448 0449 // First reset to initial type 0450 if (item.applicationCategory() != ApplicationItem::AllApplications) { 0451 item.setApplicationCategory(ApplicationItem::AllApplications); 0452 changed = true; 0453 } 0454 0455 if (list.contains(item.applicationDesktopFile())) { 0456 item.setApplicationCategory(ApplicationItem::PreferredApplication); 0457 changed = true; 0458 m_hasPreferredApps = true; 0459 Q_EMIT hasPreferredAppsChanged(); 0460 } 0461 0462 if (changed) { 0463 const int row = m_list.indexOf(item); 0464 if (row >= 0) { 0465 QModelIndex index = createIndex(row, 0, AppModel::ApplicationCategoryRole); 0466 Q_EMIT dataChanged(index, index); 0467 } 0468 } 0469 } 0470 } 0471 0472 QVariant AppModel::data(const QModelIndex &index, int role) const 0473 { 0474 const int row = index.row(); 0475 0476 if (row >= 0 && row < m_list.count()) { 0477 const ApplicationItem &item = m_list.at(row); 0478 0479 switch (role) { 0480 case ApplicationNameRole: 0481 return item.applicationName(); 0482 case ApplicationGenericNameRole: 0483 return item.applicationGenericName(); 0484 case ApplicationUntranslatedGenericNameRole: 0485 return item.applicationUntranslatedGenericName(); 0486 case ApplicationIconRole: 0487 return item.applicationIcon(); 0488 case ApplicationDesktopFileRole: 0489 return item.applicationDesktopFile(); 0490 case ApplicationCategoryRole: 0491 return static_cast<int>(item.applicationCategory()); 0492 case ApplicationSupportedMimeTypesRole: 0493 return QVariant::fromValue(item.supportedMimeTypes()); 0494 default: 0495 break; 0496 } 0497 } 0498 0499 return {}; 0500 } 0501 0502 int AppModel::rowCount(const QModelIndex &parent) const 0503 { 0504 return parent.isValid() ? 0 : m_list.count(); 0505 } 0506 0507 QHash<int, QByteArray> AppModel::roleNames() const 0508 { 0509 return { 0510 {ApplicationNameRole, QByteArrayLiteral("applicationName")}, 0511 {ApplicationIconRole, QByteArrayLiteral("applicationIcon")}, 0512 {ApplicationDesktopFileRole, QByteArrayLiteral("applicationDesktopFile")}, 0513 {ApplicationCategoryRole, QByteArrayLiteral("applicationCategory")}, 0514 }; 0515 } 0516 0517 void AppModel::loadApplications() 0518 { 0519 const KService::List appServices = KApplicationTrader::query([](const KService::Ptr &service) -> bool { 0520 return service->isValid(); 0521 }); 0522 for (const KService::Ptr &service : appServices) { 0523 if (service->noDisplay()) { 0524 if (const auto alias = service->aliasFor(); !alias.isEmpty()) { 0525 m_noDisplayAliasesFor.insert(service->desktopEntryName(), service->aliasFor()); 0526 } 0527 continue; // no display after all 0528 } 0529 0530 const QString fullName = service->property<QString>(QStringLiteral("X-GNOME-FullName")); 0531 const QString name = fullName.isEmpty() ? service->name() : fullName; 0532 ApplicationItem appItem(name, service); 0533 0534 if (!m_list.contains(appItem)) { 0535 m_list.append(appItem); 0536 } 0537 } 0538 }