You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

464 lines
14 KiB

/*
SPDX-FileCopyrightText: 2015 Eike Hein <hein@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "appentry.h"
#include "actionlist.h"
#include "appsmodel.h"
#include "containmentinterface.h"
#include <config-workspace.h>
#include <config-X11.h>
#include <QFileInfo>
#include <QProcess>
#include <QQmlPropertyMap>
#include <QStandardPaths>
#if HAVE_X11
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <private/qtx11extras_p.h>
#else
#include <QX11Info>
#endif
#endif
#include <KActivities/ResourceInstance>
#include <KApplicationTrader>
#include <KConfigGroup>
#include <KIO/ApplicationLauncherJob>
#include <KIO/CommandLauncherJob>
#include <KJob>
#include <KLocalizedString>
#include <KNotificationJobUiDelegate>
#include <KSharedConfig>
#include <KShell>
#include <KStartupInfo>
#include <KSycoca>
#include <KWindowSystem>
#include <Plasma/Plasma>
#ifdef HAVE_ICU
#include <unicode/translit.h>
#endif
namespace
{
#ifdef HAVE_ICU
std::unique_ptr<icu::Transliterator> getICUTransliterator(const QLocale &locale)
{
// Only use transliterator for certain locales.
// Because application name is a localized string, it would be really rare to
// have Chinese/Japanese character on other locales. Even if that happens, it
// is ok to use to the old 1 character strategy instead of using transliterator.
icu::UnicodeString id;
if (locale.language() == QLocale::Japanese) {
id = "Katakana-Hiragana";
} else if (locale.language() == QLocale::Chinese) {
id = "Han-Latin; Latin-ASCII";
}
if (id.isEmpty()) {
return nullptr;
}
auto ue = UErrorCode::U_ZERO_ERROR;
auto transliterator = std::unique_ptr<icu::Transliterator>(icu::Transliterator::createInstance(id, UTRANS_FORWARD, ue));
if (ue != UErrorCode::U_ZERO_ERROR) {
return nullptr;
}
return transliterator;
}
#endif
QString groupName(const QString &name)
{
if (name.isEmpty()) {
return QString();
}
const QChar firstChar = name[0];
// Put all applications whose names begin with numbers in group #
if (firstChar.isDigit()) {
return QStringLiteral("#");
}
// Put all applications whose names begin with punctuations/symbols/spaces in group &
if (firstChar.isPunct() || firstChar.isSymbol() || firstChar.isSpace()) {
return QStringLiteral("&");
}
// Here we will apply a locale based strategy for the first character.
// If first character is hangul, run decomposition and return the choseong (consonants).
if (firstChar.script() == QChar::Script_Hangul) {
auto decomposed = firstChar.decomposition();
if (decomposed.isEmpty()) {
return name.left(1);
}
return decomposed.left(1);
}
const auto locale = QLocale::system();
if (locale.language() == QLocale::Japanese) {
// We do this here for Japanese locale because:
// 1. it does not make much sense to have every different Kanji to have a different group.
// 2. ICU transliterator can't yet convert Kanji to Hiragana.
// https://unicode-org.atlassian.net/browse/ICU-5874
if (firstChar.script() == QChar::Script_Han) {
// Unicode Han
return QString::fromUtf8("\xe6\xbc\xa2");
}
}
#ifdef HAVE_ICU
// Precondition to use transliterator.
if ((locale.language() == QLocale::Chinese && firstChar.script() == QChar::Script_Han)
|| (locale.language() == QLocale::Japanese && firstChar.script() == QChar::Script_Katakana)) {
static auto transliterator = getICUTransliterator(locale);
if (transliterator) {
icu::UnicodeString icuText(reinterpret_cast<const char16_t *>(name.data()), name.size());
transliterator->transliterate(icuText);
return QString::fromUtf16(icuText.getBuffer(), static_cast<int>(icuText.length())).left(1);
}
}
#endif
return name.left(1);
}
}
AppEntry::AppEntry(AbstractModel *owner, KService::Ptr service, NameFormat nameFormat)
: AbstractEntry(owner)
, m_service(service)
{
Q_ASSERT(service);
init(nameFormat);
}
AppEntry::AppEntry(AbstractModel *owner, const QString &id)
: AbstractEntry(owner)
{
const QUrl url(id);
if (url.scheme() == QLatin1String("preferred")) {
m_service = defaultAppByName(url.host());
m_id = id;
m_con = QObject::connect(KSycoca::self(), &KSycoca::databaseChanged, owner, [this, owner, id]() {
KSharedConfig::openConfig()->reparseConfiguration();
m_service = defaultAppByName(QUrl(id).host());
if (m_service) {
init((NameFormat)owner->rootModel()->property("appNameFormat").toInt());
m_icon = QIcon();
Q_EMIT owner->layoutChanged();
}
});
} else {
m_service = KService::serviceByStorageId(id);
}
if (!m_service) {
m_service = new KService(QString());
}
if (m_service->isValid()) {
init((NameFormat)owner->rootModel()->property("appNameFormat").toInt());
}
}
void AppEntry::init(NameFormat nameFormat)
{
m_name = nameFromService(m_service, nameFormat);
if (nameFormat == GenericNameOnly) {
m_description = nameFromService(m_service, NameOnly);
} else {
m_description = nameFromService(m_service, GenericNameOnly);
}
}
bool AppEntry::isValid() const
{
return m_service->isValid();
}
QIcon AppEntry::icon() const
{
if (m_icon.isNull()) {
const QString serviceIcon = m_service->icon();
// Check for absolute-path-ness this way rather than using
// QFileInfo.isAbsolute() because that would perform a ton of unnecessary
// filesystem checks, and most icons are not defined in apps' desktop
// files with absolute paths.
bool isAbsoluteFilePath = serviceIcon.startsWith(QLatin1String("/"));
// Need to first check for whether the icon has an absolute path, because
// otherwise if the icon is just a name, QFileInfo will treat it as a
// relative path and return true if there randomly happens to be a file
// with the name of an icon in the user's homedir and we'll go down the
// wrong codepath and end up with a blank QIcon; See 457965.
if (isAbsoluteFilePath && QFileInfo::exists(serviceIcon)) {
m_icon = QIcon(serviceIcon);
} else {
m_icon = QIcon::fromTheme(serviceIcon, QIcon::fromTheme(QStringLiteral("unknown")));
}
}
return m_icon;
}
QString AppEntry::name() const
{
return m_name;
}
QString AppEntry::description() const
{
return m_description;
}
KService::Ptr AppEntry::service() const
{
return m_service;
}
QString AppEntry::group() const
{
if (m_group.isNull()) {
m_group = groupName(m_name);
if (m_group.isNull()) {
m_group = QLatin1String("");
}
Q_ASSERT(!m_group.isNull());
}
return m_group;
}
QString AppEntry::id() const
{
if (!m_id.isEmpty()) {
return m_id;
}
return m_service->storageId();
}
QString AppEntry::menuId() const
{
return m_service->menuId();
}
QUrl AppEntry::url() const
{
return QUrl::fromLocalFile(Kicker::resolvedServiceEntryPath(m_service));
}
bool AppEntry::hasActions() const
{
return true;
}
QVariantList AppEntry::actions() const
{
QVariantList actionList;
actionList << Kicker::jumpListActions(m_service);
if (!actionList.isEmpty()) {
actionList << Kicker::createSeparatorActionItem();
}
QObject *appletInterface = m_owner->rootModel()->property("appletInterface").value<QObject *>();
bool systemImmutable = false;
if (appletInterface) {
systemImmutable = (appletInterface->property("immutability").toInt() == Plasma::Types::SystemImmutable);
}
const QVariantList &addLauncherActions = Kicker::createAddLauncherActionList(appletInterface, m_service);
if (!systemImmutable && !addLauncherActions.isEmpty()) {
actionList << addLauncherActions;
}
const QVariantList &recentDocuments = Kicker::recentDocumentActions(m_service);
if (!recentDocuments.isEmpty()) {
actionList << recentDocuments << Kicker::createSeparatorActionItem();
}
const QVariantList &additionalActions = Kicker::additionalAppActions(m_service);
if (!additionalActions.isEmpty()) {
actionList << additionalActions << Kicker::createSeparatorActionItem();
}
// Don't allow adding launchers, editing, hiding, or uninstalling applications
// when system is immutable.
if (systemImmutable) {
return actionList;
}
if (m_service->isApplication()) {
actionList << Kicker::createSeparatorActionItem();
actionList << Kicker::editApplicationAction(m_service);
actionList << Kicker::appstreamActions(m_service);
}
if (appletInterface) {
QQmlPropertyMap *appletConfig = qobject_cast<QQmlPropertyMap *>(appletInterface->property("configuration").value<QObject *>());
if (appletConfig && appletConfig->contains(QStringLiteral("hiddenApplications")) && qobject_cast<AppsModel *>(m_owner)) {
const QStringList &hiddenApps = appletConfig->value(QStringLiteral("hiddenApplications")).toStringList();
if (!hiddenApps.contains(m_service->menuId())) {
QVariantMap hideAction = Kicker::createActionItem(i18n("Hide Application"), QStringLiteral("view-hidden"), QStringLiteral("hideApplication"));
actionList << hideAction;
}
}
}
return actionList;
}
bool AppEntry::run(const QString &actionId, const QVariant &argument)
{
if (!m_service->isValid()) {
return false;
}
if (actionId.isEmpty()) {
quint32 timeStamp = 0;
#if HAVE_X11
if (QX11Info::isPlatformX11()) {
timeStamp = QX11Info::appUserTime();
}
#endif
auto *job = new KIO::ApplicationLauncherJob(m_service);
job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled));
job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles);
if (KWindowSystem::isPlatformX11()) {
job->setStartupId(KStartupInfo::createNewStartupIdForTimestamp(timeStamp));
}
job->start();
KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + m_service->storageId()), QStringLiteral("org.kde.plasma.kicker"));
return true;
}
QObject *appletInterface = m_owner->rootModel()->property("appletInterface").value<QObject *>();
if (Kicker::handleAddLauncherAction(actionId, appletInterface, m_service)) {
return false; // We don't want to close Kicker, BUG: 390585
} else if (Kicker::handleEditApplicationAction(actionId, m_service)) {
return true;
} else if (Kicker::handleAppstreamActions(actionId, argument)) {
return true;
} else if (actionId == QLatin1String("_kicker_jumpListAction")) {
auto job = new KIO::CommandLauncherJob(argument.toString());
job->setDesktopName(m_service->entryPath());
job->setIcon(m_service->icon());
return job->exec();
} else if (Kicker::handleAdditionalAppActions(actionId, m_service, argument)) {
return true;
}
return Kicker::handleRecentDocumentAction(m_service, actionId, argument);
}
QString AppEntry::nameFromService(const KService::Ptr &service, NameFormat nameFormat)
{
const QString &name = service->name();
QString genericName = service->genericName();
if (genericName.isEmpty()) {
genericName = service->comment();
}
if (nameFormat == NameOnly || genericName.isEmpty() || name == genericName) {
return name;
} else if (nameFormat == GenericNameOnly) {
return genericName;
} else if (nameFormat == NameAndGenericName) {
return i18nc("App name (Generic name)", "%1 (%2)", name, genericName);
} else {
return i18nc("Generic name (App name)", "%1 (%2)", genericName, name);
}
}
KService::Ptr AppEntry::defaultAppByName(const QString &name)
{
if (name == QLatin1String("browser")) {
KConfigGroup config(KSharedConfig::openConfig(), "General");
QString browser = config.readPathEntry("BrowserApplication", QString());
if (browser.isEmpty()) {
return KApplicationTrader::preferredService(QStringLiteral("text/html"));
} else if (browser.startsWith(QLatin1Char('!'))) {
browser.remove(0, 1);
}
return KService::serviceByStorageId(browser);
}
return KService::Ptr();
}
AppEntry::~AppEntry()
{
if (m_con) {
QObject::disconnect(m_con);
}
}
AppGroupEntry::AppGroupEntry(AppsModel *parentModel,
KServiceGroup::Ptr group,
bool paginate,
int pageSize,
bool flat,
bool sorted,
bool separators,
int appNameFormat)
: AbstractGroupEntry(parentModel)
, m_group(group)
{
AppsModel *model = new AppsModel(group->entryPath(), paginate, pageSize, flat, sorted, separators, parentModel);
model->setAppNameFormat(appNameFormat);
m_childModel = model;
QObject::connect(parentModel, &AppsModel::cleared, model, &AppsModel::deleteLater);
QObject::connect(model, &AppsModel::countChanged, [parentModel, this] {
if (parentModel) {
parentModel->entryChanged(this);
}
});
QObject::connect(model, &AppsModel::hiddenEntriesChanged, [parentModel, this] {
if (parentModel) {
parentModel->entryChanged(this);
}
});
}
QIcon AppGroupEntry::icon() const
{
if (m_icon.isNull()) {
m_icon = QIcon::fromTheme(m_group->icon(), QIcon::fromTheme(QStringLiteral("unknown")));
}
return m_icon;
}
QString AppGroupEntry::name() const
{
return m_group->caption();
}
bool AppGroupEntry::hasChildren() const
{
return m_childModel && m_childModel->count() > 0;
}
AbstractModel *AppGroupEntry::childModel() const
{
return m_childModel;
}