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 }