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 }