File indexing completed on 2024-12-01 13:42:50
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 <QQmlContext> 0014 #include <QQmlEngine> 0015 #include <QQuickItem> 0016 #include <QQuickWidget> 0017 0018 #include <QApplication> 0019 #include <QDir> 0020 #include <QMimeDatabase> 0021 #include <QSettings> 0022 #include <QStandardPaths> 0023 0024 #include <KApplicationTrader> 0025 #include <KIO/MimeTypeFinderJob> 0026 #include <KLocalizedString> 0027 #include <KProcess> 0028 #include <KService> 0029 #include <algorithm> 0030 #include <iterator> 0031 #include <KBuildSycocaProgressDialog> 0032 #include <kapplicationtrader.h> 0033 0034 AppChooserDialog::AppChooserDialog(const QStringList &choices, const QString &defaultApp, const QString &fileName, const QString &mimeName, QObject *parent) 0035 : QuickDialog(parent) 0036 , m_model(new AppModel(this)) 0037 , m_appChooserData(new AppChooserData(this)) 0038 { 0039 QVariantMap props = { 0040 {"title", i18nc("@title:window", "Choose Application")}, 0041 // fileName is actually the full path, confusingly enough. But showing the 0042 // whole thing is overkill; let's just show the user the file itself 0043 {"mainText", xi18nc("@info", "Choose an application to open <filename>%1</filename>", QUrl::fromLocalFile(fileName).fileName())}, 0044 }; 0045 0046 AppFilterModel *filterModel = new AppFilterModel(this); 0047 filterModel->setSourceModel(m_model); 0048 0049 m_appChooserData->setFileName(fileName); 0050 m_appChooserData->setDefaultApp(defaultApp); 0051 filterModel->setDefaultApp(defaultApp); 0052 0053 auto findDefaultApp = [this, defaultApp, filterModel]() { 0054 if (!defaultApp.isEmpty()) { 0055 return; 0056 } 0057 KService::Ptr defaultService = KApplicationTrader::preferredService(m_appChooserData->mimeName()); 0058 if (defaultService && defaultService->isValid()) { 0059 QString id = defaultService->desktopEntryName(); 0060 m_appChooserData->setDefaultApp(id); 0061 filterModel->setDefaultApp(id); 0062 } 0063 }; 0064 0065 auto findPreferredApps = [this, choices]() { 0066 if (!choices.isEmpty()) { 0067 m_model->setPreferredApps(choices); 0068 return; 0069 } 0070 QStringList choices; 0071 const KService::List appServices = KApplicationTrader::queryByMimeType(m_appChooserData->mimeName(), [](const KService::Ptr &service) -> bool { 0072 return service->isValid(); 0073 }); 0074 std::transform(appServices.begin(), appServices.end(), std::back_inserter(choices), [](const KService::Ptr &service) { 0075 return service ? service->desktopEntryName() : QString(); 0076 }); 0077 m_model->setPreferredApps(choices); 0078 }; 0079 0080 if (mimeName.isEmpty()) { 0081 auto job = new KIO::MimeTypeFinderJob(QUrl::fromUserInput(fileName)); 0082 job->setAuthenticationPromptEnabled(false); 0083 connect(job, &KIO::MimeTypeFinderJob::result, this, [this, job, findDefaultApp, findPreferredApps]() { 0084 if (job->error() == KJob::NoError) { 0085 m_appChooserData->setMimeName(job->mimeType()); 0086 findDefaultApp(); 0087 findPreferredApps(); 0088 } else { 0089 qCWarning(XdgDesktopPortalKdeAppChooser) << "couldn't get mimetype:" << job->errorString(); 0090 } 0091 }); 0092 job->start(); 0093 } else { 0094 m_appChooserData->setMimeName(mimeName); 0095 findDefaultApp(); 0096 findPreferredApps(); 0097 } 0098 0099 qmlRegisterSingletonInstance<AppFilterModel>("org.kde.xdgdesktopportal", 1, 0, "AppModel", filterModel); 0100 qmlRegisterSingletonInstance<AppChooserData>("org.kde.xdgdesktopportal", 1, 0, "AppChooserData", m_appChooserData); 0101 0102 create(QStringLiteral("qrc:/AppChooserDialog.qml"), props); 0103 0104 connect(m_appChooserData, &AppChooserData::openDiscover, this, &AppChooserDialog::onOpenDiscover); 0105 connect(m_appChooserData, &AppChooserData::applicationSelected, this, &AppChooserDialog::onApplicationSelected); 0106 } 0107 0108 QString AppChooserDialog::selectedApplication() const 0109 { 0110 return m_selectedApplication; 0111 } 0112 0113 void AppChooserDialog::onApplicationSelected(const QString &desktopFile, const bool remember) 0114 { 0115 m_selectedApplication = desktopFile; 0116 0117 if (remember && !m_appChooserData->mimeName().isEmpty()) { 0118 KService::Ptr serv = KService::serviceByDesktopName(desktopFile); 0119 KApplicationTrader::setPreferredService(m_appChooserData->mimeName(), serv); 0120 // kbuildsycoca is the one reading mimeapps.list, so we need to run it now 0121 KBuildSycocaProgressDialog::rebuildKSycoca(QApplication::activeWindow()); 0122 } 0123 0124 accept(); 0125 } 0126 0127 void AppChooserDialog::onOpenDiscover() 0128 { 0129 QStringList args; 0130 if (!m_appChooserData->mimeName().isEmpty()) { 0131 args << QStringLiteral("--mime") << m_appChooserData->mimeName(); 0132 } 0133 KProcess::startDetached(QStringLiteral("plasma-discover"), args); 0134 } 0135 0136 void AppChooserDialog::updateChoices(const QStringList &choices) 0137 { 0138 m_model->setPreferredApps(choices); 0139 } 0140 0141 ApplicationItem::ApplicationItem(const QString &name, const QString &icon, const QString &desktopFileName) 0142 : m_applicationName(name) 0143 , m_applicationIcon(icon) 0144 , m_applicationDesktopFile(desktopFileName) 0145 , m_applicationCategory(AllApplications) 0146 { 0147 } 0148 0149 QString ApplicationItem::applicationName() const 0150 { 0151 return m_applicationName; 0152 } 0153 0154 QString ApplicationItem::applicationIcon() const 0155 { 0156 return m_applicationIcon; 0157 } 0158 0159 QString ApplicationItem::applicationDesktopFile() const 0160 { 0161 return m_applicationDesktopFile; 0162 } 0163 0164 void ApplicationItem::setApplicationCategory(ApplicationItem::ApplicationCategory category) 0165 { 0166 m_applicationCategory = category; 0167 } 0168 0169 ApplicationItem::ApplicationCategory ApplicationItem::applicationCategory() const 0170 { 0171 return m_applicationCategory; 0172 } 0173 0174 bool ApplicationItem::operator==(const ApplicationItem &item) const 0175 { 0176 return item.applicationDesktopFile() == applicationDesktopFile(); 0177 } 0178 0179 AppFilterModel::AppFilterModel(QObject *parent) 0180 : QSortFilterProxyModel(parent) 0181 { 0182 setDynamicSortFilter(true); 0183 setFilterCaseSensitivity(Qt::CaseInsensitive); 0184 sort(0, Qt::DescendingOrder); 0185 } 0186 0187 AppFilterModel::~AppFilterModel() 0188 { 0189 } 0190 0191 void AppFilterModel::setShowOnlyPrefferedApps(bool show) 0192 { 0193 m_showOnlyPreferredApps = show; 0194 0195 invalidate(); 0196 } 0197 0198 bool AppFilterModel::showOnlyPreferredApps() const 0199 { 0200 return m_showOnlyPreferredApps; 0201 } 0202 0203 void AppFilterModel::setDefaultApp(const QString &defaultApp) 0204 { 0205 m_defaultApp = defaultApp; 0206 0207 invalidate(); 0208 } 0209 0210 QString AppFilterModel::defaultApp() const 0211 { 0212 return m_defaultApp; 0213 } 0214 0215 void AppFilterModel::setFilter(const QString &text) 0216 { 0217 m_filter = text; 0218 0219 if (!m_filter.isEmpty() && m_showOnlyPreferredApps) { 0220 m_showOnlyPreferredApps = false; 0221 emit showOnlyPreferredAppsChanged(); 0222 } 0223 0224 invalidate(); 0225 } 0226 0227 QString AppFilterModel::filter() const 0228 { 0229 return m_filter; 0230 } 0231 0232 bool AppFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const 0233 { 0234 const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); 0235 0236 ApplicationItem::ApplicationCategory category = 0237 static_cast<ApplicationItem::ApplicationCategory>(sourceModel()->data(index, AppModel::ApplicationCategoryRole).toInt()); 0238 QString appName = sourceModel()->data(index, AppModel::ApplicationNameRole).toString(); 0239 0240 if (m_filter.isEmpty()) { 0241 return m_showOnlyPreferredApps ? category == ApplicationItem::PreferredApplication : true; 0242 } 0243 0244 return m_showOnlyPreferredApps ? category == ApplicationItem::PreferredApplication && appName.contains(m_filter, Qt::CaseInsensitive) 0245 : appName.contains(m_filter, Qt::CaseInsensitive); 0246 } 0247 0248 bool AppFilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const 0249 { 0250 if (sourceModel()->data(left, AppModel::ApplicationDesktopFileRole) == m_defaultApp) { 0251 return false; 0252 } 0253 if (sourceModel()->data(right, AppModel::ApplicationDesktopFileRole) == m_defaultApp) { 0254 return true; 0255 } 0256 ApplicationItem::ApplicationCategory leftCategory = 0257 static_cast<ApplicationItem::ApplicationCategory>(sourceModel()->data(left, AppModel::ApplicationCategoryRole).toInt()); 0258 ApplicationItem::ApplicationCategory rightCategory = 0259 static_cast<ApplicationItem::ApplicationCategory>(sourceModel()->data(right, AppModel::ApplicationCategoryRole).toInt()); 0260 QString leftName = sourceModel()->data(left, AppModel::ApplicationNameRole).toString(); 0261 QString rightName = sourceModel()->data(right, AppModel::ApplicationNameRole).toString(); 0262 0263 if (leftCategory < rightCategory) { 0264 return false; 0265 } else if (leftCategory > rightCategory) { 0266 return true; 0267 } 0268 0269 return QString::localeAwareCompare(leftName, rightName) > 0; 0270 } 0271 0272 QString AppChooserData::defaultApp() const 0273 { 0274 return m_defaultApp; 0275 } 0276 0277 void AppChooserData::setDefaultApp(const QString &defaultApp) 0278 { 0279 m_defaultApp = defaultApp; 0280 Q_EMIT defaultAppChanged(); 0281 } 0282 0283 AppChooserData::AppChooserData(QObject *parent) 0284 : QObject(parent) 0285 { 0286 } 0287 0288 QString AppChooserData::fileName() const 0289 { 0290 return m_fileName; 0291 } 0292 0293 void AppChooserData::setFileName(const QString &fileName) 0294 { 0295 m_fileName = fileName; 0296 Q_EMIT fileNameChanged(); 0297 } 0298 0299 QString AppChooserData::mimeName() const 0300 { 0301 return m_mimeName; 0302 } 0303 0304 void AppChooserData::setMimeName(const QString &mimeName) 0305 { 0306 if (m_mimeName != mimeName) { 0307 m_mimeName = mimeName; 0308 Q_EMIT mimeNameChanged(); 0309 Q_EMIT mimeDescChanged(); 0310 } 0311 } 0312 0313 QString AppChooserData::mimeDesc() const 0314 { 0315 return QMimeDatabase().mimeTypeForName(m_mimeName).comment(); 0316 } 0317 0318 AppModel::AppModel(QObject *parent) 0319 : QAbstractListModel(parent) 0320 { 0321 loadApplications(); 0322 } 0323 0324 AppModel::~AppModel() 0325 { 0326 } 0327 0328 void AppModel::setPreferredApps(const QStringList &possiblyAliasedList) 0329 { 0330 m_hasPreferredApps = false; 0331 Q_EMIT hasPreferredAppsChanged(); 0332 0333 // In the event that we get incoming NoDisplay entries that are AliasFor another desktop file, 0334 // switch the NoDisplay name for the aliased name. 0335 QStringList list; 0336 for (const auto &entry : possiblyAliasedList) { 0337 if (const auto value = m_noDisplayAliasesFor.value(entry); !value.isEmpty()) { 0338 list << value; 0339 } else { 0340 list << entry; 0341 } 0342 } 0343 0344 for (ApplicationItem &item : m_list) { 0345 bool changed = false; 0346 0347 // First reset to initial type 0348 if (item.applicationCategory() != ApplicationItem::AllApplications) { 0349 item.setApplicationCategory(ApplicationItem::AllApplications); 0350 changed = true; 0351 } 0352 0353 if (list.contains(item.applicationDesktopFile())) { 0354 item.setApplicationCategory(ApplicationItem::PreferredApplication); 0355 changed = true; 0356 m_hasPreferredApps = true; 0357 Q_EMIT hasPreferredAppsChanged(); 0358 } 0359 0360 if (changed) { 0361 const int row = m_list.indexOf(item); 0362 if (row >= 0) { 0363 QModelIndex index = createIndex(row, 0, AppModel::ApplicationCategoryRole); 0364 Q_EMIT dataChanged(index, index); 0365 } 0366 } 0367 } 0368 } 0369 0370 QVariant AppModel::data(const QModelIndex &index, int role) const 0371 { 0372 const int row = index.row(); 0373 0374 if (row >= 0 && row < m_list.count()) { 0375 ApplicationItem item = m_list.at(row); 0376 0377 switch (role) { 0378 case ApplicationNameRole: 0379 return item.applicationName(); 0380 case ApplicationIconRole: 0381 return item.applicationIcon(); 0382 case ApplicationDesktopFileRole: 0383 return item.applicationDesktopFile(); 0384 case ApplicationCategoryRole: 0385 return static_cast<int>(item.applicationCategory()); 0386 default: 0387 break; 0388 } 0389 } 0390 0391 return QVariant(); 0392 } 0393 0394 int AppModel::rowCount(const QModelIndex &parent) const 0395 { 0396 return parent.isValid() ? 0 : m_list.count(); 0397 } 0398 0399 QHash<int, QByteArray> AppModel::roleNames() const 0400 { 0401 return { 0402 {ApplicationNameRole, QByteArrayLiteral("applicationName")}, 0403 {ApplicationIconRole, QByteArrayLiteral("applicationIcon")}, 0404 {ApplicationDesktopFileRole, QByteArrayLiteral("applicationDesktopFile")}, 0405 {ApplicationCategoryRole, QByteArrayLiteral("applicationCategory")}, 0406 }; 0407 } 0408 0409 void AppModel::loadApplications() 0410 { 0411 const KService::List appServices = KApplicationTrader::query([](const KService::Ptr &service) -> bool { 0412 return service->isValid(); 0413 }); 0414 for (const KService::Ptr &service : appServices) { 0415 if (service->noDisplay()) { 0416 if (const auto alias = service->aliasFor(); !alias.isEmpty()) { 0417 m_noDisplayAliasesFor.insert(service->desktopEntryName(), service->aliasFor()); 0418 } 0419 continue; // no display after all 0420 } 0421 0422 const QString fullName = service->property(QStringLiteral("X-GNOME-FullName"), QMetaType::QString).toString(); 0423 const QString name = fullName.isEmpty() ? service->name() : fullName; 0424 ApplicationItem appItem(name, service->icon(), service->desktopEntryName()); 0425 0426 if (!m_list.contains(appItem)) { 0427 m_list.append(appItem); 0428 } 0429 } 0430 }