File indexing completed on 2024-06-09 05:30:56

0001 /*
0002     SPDX-FileCopyrightText: 2015 Eike Hein <hein@kde.org>
0003     SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "appentry.h"
0009 #include "actionlist.h"
0010 #include "appsmodel.h"
0011 #include "containmentinterface.h"
0012 #include <config-workspace.h>
0013 
0014 #include <QFileInfo>
0015 #include <QProcess>
0016 #include <QQmlPropertyMap>
0017 #include <QStandardPaths>
0018 
0019 #include <KApplicationTrader>
0020 #include <KConfigGroup>
0021 #include <KIO/ApplicationLauncherJob>
0022 #include <KJob>
0023 #include <KLocalizedString>
0024 #include <KNotificationJobUiDelegate>
0025 #include <KSharedConfig>
0026 #include <KShell>
0027 #include <KSycoca>
0028 #include <KWindowSystem>
0029 #include <PlasmaActivities/ResourceInstance>
0030 
0031 #include <defaultservice.h>
0032 
0033 #include <Plasma/Plasma>
0034 
0035 #ifdef HAVE_ICU
0036 #include <unicode/translit.h>
0037 #endif
0038 
0039 namespace
0040 {
0041 
0042 #ifdef HAVE_ICU
0043 std::unique_ptr<icu::Transliterator> getICUTransliterator(const QLocale &locale)
0044 {
0045     // Only use transliterator for certain locales.
0046     // Because application name is a localized string, it would be really rare to
0047     // have Chinese/Japanese character on other locales. Even if that happens, it
0048     // is ok to use to the old 1 character strategy instead of using transliterator.
0049     icu::UnicodeString id;
0050     if (locale.language() == QLocale::Japanese) {
0051         id = "Katakana-Hiragana";
0052     } else if (locale.language() == QLocale::Chinese) {
0053         id = "Han-Latin; Latin-ASCII";
0054     }
0055     if (id.isEmpty()) {
0056         return nullptr;
0057     }
0058     auto ue = UErrorCode::U_ZERO_ERROR;
0059     auto transliterator = std::unique_ptr<icu::Transliterator>(icu::Transliterator::createInstance(id, UTRANS_FORWARD, ue));
0060 
0061     if (ue != UErrorCode::U_ZERO_ERROR) {
0062         return nullptr;
0063     }
0064 
0065     return transliterator;
0066 }
0067 #endif
0068 
0069 QString groupName(const QString &name)
0070 {
0071     if (name.isEmpty()) {
0072         return QString();
0073     }
0074 
0075     const QChar firstChar = name[0];
0076 
0077     // Put all applications whose names begin with numbers in group #
0078     if (firstChar.isDigit()) {
0079         return QStringLiteral("#");
0080     }
0081 
0082     // Put all applications whose names begin with punctuations/symbols/spaces in group &
0083     if (firstChar.isPunct() || firstChar.isSymbol() || firstChar.isSpace()) {
0084         return QStringLiteral("&");
0085     }
0086 
0087     // Here we will apply a locale based strategy for the first character.
0088     // If first character is hangul, run decomposition and return the choseong (consonants).
0089     if (firstChar.script() == QChar::Script_Hangul) {
0090         auto decomposed = firstChar.decomposition();
0091         if (decomposed.isEmpty()) {
0092             return name.left(1);
0093         }
0094         return decomposed.left(1);
0095     }
0096     const auto locale = QLocale::system();
0097     if (locale.language() == QLocale::Japanese) {
0098         // We do this here for Japanese locale because:
0099         // 1. it does not make much sense to have every different Kanji to have a different group.
0100         // 2. ICU transliterator can't yet convert Kanji to Hiragana.
0101         //    https://unicode-org.atlassian.net/browse/ICU-5874
0102         if (firstChar.script() == QChar::Script_Han) {
0103             // Unicode Han
0104             return QString::fromUtf8("\xe6\xbc\xa2");
0105         }
0106     }
0107 #ifdef HAVE_ICU
0108     // Precondition to use transliterator.
0109     if ((locale.language() == QLocale::Chinese && firstChar.script() == QChar::Script_Han)
0110         || (locale.language() == QLocale::Japanese && firstChar.script() == QChar::Script_Katakana)) {
0111         static auto transliterator = getICUTransliterator(locale);
0112 
0113         if (transliterator) {
0114             icu::UnicodeString icuText(reinterpret_cast<const char16_t *>(name.data()), name.size());
0115             transliterator->transliterate(icuText);
0116             return QString::fromUtf16(icuText.getBuffer(), static_cast<int>(icuText.length())).left(1);
0117         }
0118     }
0119 #endif
0120     return name.left(1);
0121 }
0122 }
0123 
0124 AppEntry::AppEntry(AbstractModel *owner, KService::Ptr service, NameFormat nameFormat)
0125     : AbstractEntry(owner)
0126     , m_service(service)
0127 {
0128     Q_ASSERT(service);
0129     init(nameFormat);
0130 }
0131 
0132 AppEntry::AppEntry(AbstractModel *owner, const QString &id)
0133     : AbstractEntry(owner)
0134 {
0135     const QUrl url(id);
0136     if (url.scheme() == QLatin1String("preferred")) {
0137         m_service = defaultAppByName(url.host());
0138         m_id = id;
0139     } else {
0140         m_service = KService::serviceByStorageId(id);
0141     }
0142     if (!m_service) {
0143         m_service = new KService(QString());
0144     }
0145 
0146     m_con = QObject::connect(KSycoca::self(), &KSycoca::databaseChanged, owner, [this, owner, id]() {
0147         const QUrl url(id);
0148         if (url.scheme() == QLatin1String("preferred")) {
0149             KSharedConfig::openConfig()->reparseConfiguration();
0150             m_service = defaultAppByName(url.host());
0151             if (m_service) {
0152                 init((NameFormat)owner->rootModel()->property("appNameFormat").toInt());
0153                 m_icon = QString();
0154                 Q_EMIT owner->layoutChanged();
0155             }
0156         } else {
0157             m_service = KService::serviceByStorageId(id);
0158             init((NameFormat)owner->rootModel()->property("appNameFormat").toInt());
0159             m_icon = QString();
0160             Q_EMIT owner->layoutChanged();
0161         }
0162         if (!m_service) {
0163             m_service = new KService(QString());
0164         }
0165     });
0166 
0167     if (m_service->isValid()) {
0168         init((NameFormat)owner->rootModel()->property("appNameFormat").toInt());
0169     }
0170 }
0171 
0172 void AppEntry::init(NameFormat nameFormat)
0173 {
0174     m_name = nameFromService(m_service, nameFormat);
0175 
0176     if (nameFormat == GenericNameOnly) {
0177         m_description = nameFromService(m_service, NameOnly);
0178     } else {
0179         m_description = nameFromService(m_service, GenericNameOnly);
0180     }
0181 }
0182 
0183 bool AppEntry::isValid() const
0184 {
0185     return m_service->isValid();
0186 }
0187 
0188 QString AppEntry::icon() const
0189 {
0190     if (m_icon.isNull()) {
0191         m_icon = m_service->icon();
0192     }
0193     return m_icon;
0194 }
0195 
0196 QString AppEntry::name() const
0197 {
0198     return m_name;
0199 }
0200 
0201 QString AppEntry::description() const
0202 {
0203     return m_description;
0204 }
0205 
0206 KService::Ptr AppEntry::service() const
0207 {
0208     return m_service;
0209 }
0210 
0211 QString AppEntry::group() const
0212 {
0213     if (m_group.isNull()) {
0214         m_group = groupName(m_name);
0215         if (m_group.isNull()) {
0216             m_group = QLatin1String("");
0217         }
0218         Q_ASSERT(!m_group.isNull());
0219     }
0220     return m_group;
0221 }
0222 
0223 QString AppEntry::id() const
0224 {
0225     if (!m_id.isEmpty()) {
0226         return m_id;
0227     }
0228 
0229     return m_service->storageId();
0230 }
0231 
0232 QString AppEntry::menuId() const
0233 {
0234     return m_service->menuId();
0235 }
0236 
0237 QUrl AppEntry::url() const
0238 {
0239     return QUrl::fromLocalFile(m_service->entryPath());
0240 }
0241 
0242 bool AppEntry::hasActions() const
0243 {
0244     return true;
0245 }
0246 
0247 QVariantList AppEntry::actions() const
0248 {
0249     QVariantList actionList;
0250 
0251     actionList << Kicker::jumpListActions(m_service);
0252     if (!actionList.isEmpty()) {
0253         actionList << Kicker::createSeparatorActionItem();
0254     }
0255 
0256     QObject *appletInterface = m_owner->rootModel()->property("appletInterface").value<QObject *>();
0257 
0258     bool systemImmutable = false;
0259     if (appletInterface) {
0260         systemImmutable = (appletInterface->property("immutability").toInt() == Plasma::Types::SystemImmutable);
0261     }
0262 
0263     const QVariantList &addLauncherActions = Kicker::createAddLauncherActionList(appletInterface, m_service);
0264     if (!systemImmutable && !addLauncherActions.isEmpty()) {
0265         actionList << addLauncherActions;
0266     }
0267 
0268     const QVariantList &recentDocuments = Kicker::recentDocumentActions(m_service);
0269     if (!recentDocuments.isEmpty()) {
0270         actionList << recentDocuments << Kicker::createSeparatorActionItem();
0271     }
0272 
0273     const QVariantList &additionalActions = Kicker::additionalAppActions(m_service);
0274     if (!additionalActions.isEmpty()) {
0275         actionList << additionalActions << Kicker::createSeparatorActionItem();
0276     }
0277 
0278     // Don't allow adding launchers, editing, hiding, or uninstalling applications
0279     // when system is immutable.
0280     if (systemImmutable) {
0281         return actionList;
0282     }
0283 
0284     if (m_service->isApplication()) {
0285         actionList << Kicker::createSeparatorActionItem();
0286         actionList << Kicker::editApplicationAction(m_service);
0287         actionList << Kicker::appstreamActions(m_service);
0288     }
0289 
0290     if (appletInterface) {
0291         QQmlPropertyMap *appletConfig = qobject_cast<QQmlPropertyMap *>(appletInterface->property("configuration").value<QObject *>());
0292 
0293         if (appletConfig && appletConfig->contains(QStringLiteral("hiddenApplications")) && qobject_cast<AppsModel *>(m_owner)) {
0294             const QStringList &hiddenApps = appletConfig->value(QStringLiteral("hiddenApplications")).toStringList();
0295 
0296             if (!hiddenApps.contains(m_service->menuId())) {
0297                 QVariantMap hideAction = Kicker::createActionItem(i18n("Hide Application"), QStringLiteral("view-hidden"), QStringLiteral("hideApplication"));
0298                 actionList << hideAction;
0299             }
0300         }
0301     }
0302 
0303     return actionList;
0304 }
0305 
0306 bool AppEntry::run(const QString &actionId, const QVariant &argument)
0307 {
0308     if (!m_service->isValid()) {
0309         return false;
0310     }
0311 
0312     if (actionId.isEmpty()) {
0313         auto *job = new KIO::ApplicationLauncherJob(m_service);
0314         job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled));
0315         job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles);
0316         job->start();
0317 
0318         KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + m_service->storageId()), QStringLiteral("org.kde.plasma.kicker"));
0319 
0320         return true;
0321     }
0322 
0323     QObject *appletInterface = m_owner->rootModel()->property("appletInterface").value<QObject *>();
0324 
0325     if (Kicker::handleAddLauncherAction(actionId, appletInterface, m_service)) {
0326         return false; // We don't want to close Kicker, BUG: 390585
0327     } else if (Kicker::handleEditApplicationAction(actionId, m_service)) {
0328         return true;
0329     } else if (Kicker::handleAppstreamActions(actionId, m_service)) {
0330         return true;
0331     } else if (actionId == QLatin1String("_kicker_jumpListAction")) {
0332         auto *job = new KIO::ApplicationLauncherJob(argument.value<KServiceAction>());
0333         job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled));
0334         return job->exec();
0335     } else if (Kicker::handleAdditionalAppActions(actionId, m_service, argument)) {
0336         return true;
0337     }
0338 
0339     return Kicker::handleRecentDocumentAction(m_service, actionId, argument);
0340 }
0341 
0342 QString AppEntry::nameFromService(const KService::Ptr &service, NameFormat nameFormat)
0343 {
0344     const QString &name = service->name();
0345     QString genericName = service->genericName();
0346 
0347     if (genericName.isEmpty()) {
0348         genericName = service->comment();
0349     }
0350 
0351     if (nameFormat == NameOnly || genericName.isEmpty() || name == genericName) {
0352         return name;
0353     } else if (nameFormat == GenericNameOnly) {
0354         return genericName;
0355     } else if (nameFormat == NameAndGenericName) {
0356         return i18nc("App name (Generic name)", "%1 (%2)", name, genericName);
0357     } else {
0358         return i18nc("Generic name (App name)", "%1 (%2)", genericName, name);
0359     }
0360 }
0361 
0362 KService::Ptr AppEntry::defaultAppByName(const QString &name)
0363 {
0364     Q_UNUSED(name)
0365     return DefaultService::browser();
0366 }
0367 
0368 AppEntry::~AppEntry()
0369 {
0370     if (m_con) {
0371         QObject::disconnect(m_con);
0372     }
0373 }
0374 
0375 AppGroupEntry::AppGroupEntry(AppsModel *parentModel,
0376                              KServiceGroup::Ptr group,
0377                              bool paginate,
0378                              int pageSize,
0379                              bool flat,
0380                              bool sorted,
0381                              bool separators,
0382                              int appNameFormat)
0383     : AbstractGroupEntry(parentModel)
0384     , m_group(group)
0385 {
0386     AppsModel *model = new AppsModel(group->entryPath(), paginate, pageSize, flat, sorted, separators, parentModel);
0387     model->setAppNameFormat(appNameFormat);
0388     m_childModel = model;
0389 
0390     QObject::connect(parentModel, &AppsModel::cleared, model, &AppsModel::deleteLater);
0391 
0392     QObject::connect(model, &AppsModel::countChanged, [parentModel, this] {
0393         if (parentModel) {
0394             parentModel->entryChanged(this);
0395         }
0396     });
0397 
0398     QObject::connect(model, &AppsModel::hiddenEntriesChanged, [parentModel, this] {
0399         if (parentModel) {
0400             parentModel->entryChanged(this);
0401         }
0402     });
0403 }
0404 
0405 QString AppGroupEntry::icon() const
0406 {
0407     if (m_icon.isNull()) {
0408         m_icon = m_group->icon();
0409     }
0410     return m_icon;
0411 }
0412 
0413 QString AppGroupEntry::name() const
0414 {
0415     return m_group->caption();
0416 }
0417 
0418 bool AppGroupEntry::hasChildren() const
0419 {
0420     return m_childModel && m_childModel->count() > 0;
0421 }
0422 
0423 AbstractModel *AppGroupEntry::childModel() const
0424 {
0425     return m_childModel;
0426 }