Advance Wayland and KDE package bring-up
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# KI18N Translation Domain for this library
|
||||
add_definitions(-DTRANSLATION_DOMAIN=\"kcmkwincommon\")
|
||||
|
||||
set(kcmkwincommon_SRC
|
||||
effectsmodel.cpp
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(kcmkwincommon_SRC
|
||||
${KWin_SOURCE_DIR}/src/org.kde.kwin.Effects.xml kwin_effects_interface
|
||||
)
|
||||
|
||||
add_library(kcmkwincommon SHARED ${kcmkwincommon_SRC})
|
||||
|
||||
target_link_libraries(kcmkwincommon
|
||||
Qt::Core
|
||||
Qt::DBus
|
||||
KF6::CoreAddons
|
||||
KF6::ConfigCore
|
||||
KF6::I18n
|
||||
KF6::Package
|
||||
KF6::KCMUtils
|
||||
)
|
||||
|
||||
set_target_properties(kcmkwincommon PROPERTIES
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION 6
|
||||
)
|
||||
|
||||
install(TARGETS kcmkwincommon ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP)
|
||||
|
||||
set(kcm_kwin4_genericscripted_SRCS genericscriptedconfig.cpp)
|
||||
qt_add_dbus_interface(kcm_kwin4_genericscripted_SRCS ${kwin_effects_dbus_xml} kwineffects_interface)
|
||||
add_library(kcm_kwin4_genericscripted MODULE ${kcm_kwin4_genericscripted_SRCS})
|
||||
target_link_libraries(kcm_kwin4_genericscripted
|
||||
KF6::KCMUtils #KCModule
|
||||
KF6::I18n
|
||||
Qt::DBus
|
||||
Qt::UiTools
|
||||
)
|
||||
install(TARGETS kcm_kwin4_genericscripted DESTINATION ${KDE_INSTALL_PLUGINDIR}/kwin/effects/configs)
|
||||
@@ -0,0 +1,2 @@
|
||||
#! /usr/bin/env bash
|
||||
$XGETTEXT `find . -name \*.cpp` -o $podir/kcmkwincommon.pot
|
||||
@@ -0,0 +1,610 @@
|
||||
/*
|
||||
KWin - the KDE window manager
|
||||
This file is part of the KDE project.
|
||||
|
||||
SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas <kok3rs@gmail.com>
|
||||
SPDX-FileCopyrightText: 2018 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
#include "effectsmodel.h"
|
||||
|
||||
#include "config-kwin.h"
|
||||
|
||||
#include <kwin_effects_interface.h>
|
||||
|
||||
#include <KAboutData>
|
||||
#include <KCMultiDialog>
|
||||
#include <KConfigGroup>
|
||||
#include <KLocalizedString>
|
||||
#include <KPackage/PackageLoader>
|
||||
#include <KPluginMetaData>
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusPendingCall>
|
||||
#include <QDirIterator>
|
||||
#include <QStandardPaths>
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
|
||||
static QString translatedCategory(const QString &category)
|
||||
{
|
||||
static const QList<QString> knownCategories = {
|
||||
QStringLiteral("Accessibility"),
|
||||
QStringLiteral("Appearance"),
|
||||
QStringLiteral("Focus"),
|
||||
QStringLiteral("Show Desktop Animation"),
|
||||
QStringLiteral("Tools"),
|
||||
QStringLiteral("Virtual Desktop Switching Animation"),
|
||||
QStringLiteral("Window Management"),
|
||||
QStringLiteral("Window Open/Close Animation")};
|
||||
|
||||
static const QList<QString> translatedCategories = {
|
||||
i18nc("Category of Desktop Effects, used as section header", "Accessibility"),
|
||||
i18nc("Category of Desktop Effects, used as section header", "Appearance"),
|
||||
i18nc("Category of Desktop Effects, used as section header", "Focus"),
|
||||
i18nc("Category of Desktop Effects, used as section header", "Peek at Desktop Animation"),
|
||||
i18nc("Category of Desktop Effects, used as section header", "Tools"),
|
||||
i18nc("Category of Desktop Effects, used as section header", "Virtual Desktop Switching Animation"),
|
||||
i18nc("Category of Desktop Effects, used as section header", "Window Management"),
|
||||
i18nc("Category of Desktop Effects, used as section header", "Window Open/Close Animation")};
|
||||
|
||||
const int index = knownCategories.indexOf(category);
|
||||
if (index == -1) {
|
||||
qDebug() << "Unknown category '" << category << "' and thus not translated";
|
||||
return category;
|
||||
}
|
||||
|
||||
return translatedCategories[index];
|
||||
}
|
||||
|
||||
static EffectsModel::Status effectStatus(bool enabled)
|
||||
{
|
||||
return enabled ? EffectsModel::Status::Enabled : EffectsModel::Status::Disabled;
|
||||
}
|
||||
|
||||
EffectsModel::EffectsModel(QObject *parent)
|
||||
: QAbstractItemModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> EffectsModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roleNames;
|
||||
roleNames[NameRole] = "NameRole";
|
||||
roleNames[DescriptionRole] = "DescriptionRole";
|
||||
roleNames[AuthorNameRole] = "AuthorNameRole";
|
||||
roleNames[AuthorEmailRole] = "AuthorEmailRole";
|
||||
roleNames[LicenseRole] = "LicenseRole";
|
||||
roleNames[VersionRole] = "VersionRole";
|
||||
roleNames[CategoryRole] = "CategoryRole";
|
||||
roleNames[ServiceNameRole] = "ServiceNameRole";
|
||||
roleNames[IconNameRole] = "IconNameRole";
|
||||
roleNames[StatusRole] = "StatusRole";
|
||||
roleNames[VideoRole] = "VideoRole";
|
||||
roleNames[WebsiteRole] = "WebsiteRole";
|
||||
roleNames[SupportedRole] = "SupportedRole";
|
||||
roleNames[ExclusiveRole] = "ExclusiveRole";
|
||||
roleNames[ConfigurableRole] = "ConfigurableRole";
|
||||
roleNames[EnabledByDefaultRole] = "EnabledByDefaultRole";
|
||||
roleNames[EnabledByDefaultFunctionRole] = "EnabledByDefaultFunctionRole";
|
||||
roleNames[ConfigModuleRole] = "ConfigModuleRole";
|
||||
return roleNames;
|
||||
}
|
||||
|
||||
QModelIndex EffectsModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid() || column > 0 || column < 0 || row < 0 || row >= m_effects.count()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return createIndex(row, column);
|
||||
}
|
||||
|
||||
QModelIndex EffectsModel::parent(const QModelIndex &child) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
int EffectsModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
int EffectsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
return m_effects.count();
|
||||
}
|
||||
|
||||
QVariant EffectsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const EffectData effect = m_effects.at(index.row());
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
case NameRole:
|
||||
return effect.name;
|
||||
case DescriptionRole:
|
||||
return effect.description;
|
||||
case AuthorNameRole:
|
||||
return effect.authorName;
|
||||
case AuthorEmailRole:
|
||||
return effect.authorEmail;
|
||||
case LicenseRole:
|
||||
return effect.license;
|
||||
case VersionRole:
|
||||
return effect.version;
|
||||
case CategoryRole:
|
||||
return effect.category;
|
||||
case ServiceNameRole:
|
||||
return effect.serviceName;
|
||||
case IconNameRole:
|
||||
return effect.iconName;
|
||||
case StatusRole:
|
||||
return static_cast<int>(effect.status);
|
||||
case VideoRole:
|
||||
return effect.video;
|
||||
case WebsiteRole:
|
||||
return effect.website;
|
||||
case SupportedRole:
|
||||
return effect.supported;
|
||||
case ExclusiveRole:
|
||||
return effect.exclusiveGroup;
|
||||
case InternalRole:
|
||||
return effect.internal;
|
||||
case ConfigurableRole:
|
||||
return !effect.configModule.isEmpty();
|
||||
case EnabledByDefaultRole:
|
||||
return effect.enabledByDefault;
|
||||
case EnabledByDefaultFunctionRole:
|
||||
return effect.enabledByDefaultFunction;
|
||||
case ConfigModuleRole:
|
||||
return effect.configModule;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
bool EffectsModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return QAbstractItemModel::setData(index, value, role);
|
||||
}
|
||||
|
||||
if (role == StatusRole) {
|
||||
// note: whenever the StatusRole is modified (even to the same value) the entry
|
||||
// gets marked as changed and will get saved to the config file. This means the
|
||||
// config file could get polluted
|
||||
EffectData &data = m_effects[index.row()];
|
||||
data.status = Status(value.toInt());
|
||||
data.changed = data.status != data.originalStatus;
|
||||
Q_EMIT dataChanged(index, index);
|
||||
|
||||
if (data.status == Status::Enabled && !data.exclusiveGroup.isEmpty()) {
|
||||
// need to disable all other exclusive effects in the same category
|
||||
for (int i = 0; i < m_effects.size(); ++i) {
|
||||
if (i == index.row()) {
|
||||
continue;
|
||||
}
|
||||
EffectData &otherData = m_effects[i];
|
||||
if (otherData.exclusiveGroup == data.exclusiveGroup) {
|
||||
otherData.status = Status::Disabled;
|
||||
otherData.changed = otherData.status != otherData.originalStatus;
|
||||
Q_EMIT dataChanged(this->index(i, 0), this->index(i, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return QAbstractItemModel::setData(index, value, role);
|
||||
}
|
||||
|
||||
void EffectsModel::loadBuiltInEffects(const KConfigGroup &kwinConfig)
|
||||
{
|
||||
const QString rootDirectory = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
|
||||
QStringLiteral("kwin/builtin-effects"),
|
||||
QStandardPaths::LocateDirectory);
|
||||
|
||||
const QStringList nameFilters{QStringLiteral("*.json")};
|
||||
QDirIterator it(rootDirectory, nameFilters, QDir::Files);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
|
||||
const KPluginMetaData metaData = KPluginMetaData::fromJsonFile(it.filePath());
|
||||
if (!metaData.isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EffectData effect;
|
||||
effect.name = metaData.name();
|
||||
effect.description = metaData.description();
|
||||
effect.authorName = i18n("KWin development team");
|
||||
effect.authorEmail = QString(); // not used at all
|
||||
effect.license = metaData.license();
|
||||
effect.version = metaData.version();
|
||||
effect.untranslatedCategory = metaData.category();
|
||||
effect.category = translatedCategory(metaData.category());
|
||||
effect.serviceName = metaData.pluginId();
|
||||
effect.iconName = metaData.iconName();
|
||||
effect.enabledByDefault = metaData.isEnabledByDefault();
|
||||
effect.supported = true;
|
||||
effect.enabledByDefaultFunction = false;
|
||||
effect.internal = false;
|
||||
effect.configModule = metaData.value(QStringLiteral("X-KDE-ConfigModule"));
|
||||
effect.website = QUrl(metaData.website());
|
||||
|
||||
if (metaData.rawData().contains("org.kde.kwin.effect")) {
|
||||
const QJsonObject d(metaData.rawData().value("org.kde.kwin.effect").toObject());
|
||||
effect.exclusiveGroup = d.value("exclusiveGroup").toString();
|
||||
effect.video = QUrl::fromUserInput(d.value("video").toString());
|
||||
effect.enabledByDefaultFunction = d.value("enabledByDefaultMethod").toBool();
|
||||
effect.internal = d.value("internal").toBool();
|
||||
}
|
||||
|
||||
const QString enabledKey = QStringLiteral("%1Enabled").arg(effect.serviceName);
|
||||
if (kwinConfig.hasKey(enabledKey)) {
|
||||
effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", effect.enabledByDefault));
|
||||
} else if (effect.enabledByDefaultFunction) {
|
||||
effect.status = Status::EnabledUndeterminded;
|
||||
} else {
|
||||
effect.status = effectStatus(effect.enabledByDefault);
|
||||
}
|
||||
|
||||
effect.originalStatus = effect.status;
|
||||
|
||||
if (shouldStore(effect)) {
|
||||
m_pendingEffects << effect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EffectsModel::loadJavascriptEffects(const KConfigGroup &kwinConfig)
|
||||
{
|
||||
const auto plugins = KPackage::PackageLoader::self()->listPackages(
|
||||
QStringLiteral("KWin/Effect"),
|
||||
QStringLiteral("kwin/effects"));
|
||||
for (const KPluginMetaData &plugin : plugins) {
|
||||
EffectData effect;
|
||||
|
||||
effect.name = plugin.name();
|
||||
effect.description = plugin.description();
|
||||
const auto authors = plugin.authors();
|
||||
effect.authorName = !authors.isEmpty() ? authors.first().name() : QString();
|
||||
effect.authorEmail = !authors.isEmpty() ? authors.first().emailAddress() : QString();
|
||||
effect.license = plugin.license();
|
||||
effect.version = plugin.version();
|
||||
effect.untranslatedCategory = plugin.category();
|
||||
effect.category = translatedCategory(plugin.category());
|
||||
effect.serviceName = plugin.pluginId();
|
||||
effect.iconName = plugin.iconName();
|
||||
effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", plugin.isEnabledByDefault()));
|
||||
effect.originalStatus = effect.status;
|
||||
effect.enabledByDefault = plugin.isEnabledByDefault();
|
||||
effect.enabledByDefaultFunction = false;
|
||||
effect.video = QUrl(plugin.value(QStringLiteral("X-KWin-Video-Url")));
|
||||
effect.website = QUrl(plugin.website());
|
||||
effect.supported = true;
|
||||
effect.exclusiveGroup = plugin.value(QStringLiteral("X-KWin-Exclusive-Category"));
|
||||
effect.internal = plugin.value(QStringLiteral("X-KWin-Internal"), false);
|
||||
|
||||
if (const QString configModule = plugin.value(QStringLiteral("X-KDE-ConfigModule")); !configModule.isEmpty()) {
|
||||
if (configModule == QLatin1StringView("kcm_kwin4_genericscripted")) {
|
||||
const QString xmlFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + plugin.pluginId() + QLatin1String("/contents/config/main.xml"));
|
||||
const QString uiFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + plugin.pluginId() + QLatin1String("/contents/ui/config.ui"));
|
||||
if (QFileInfo::exists(xmlFile) && QFileInfo::exists(uiFile)) {
|
||||
effect.configModule = configModule;
|
||||
effect.configArgs = QVariantList{plugin.pluginId(), QStringLiteral("KWin/Effect")};
|
||||
}
|
||||
} else {
|
||||
effect.configModule = configModule;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldStore(effect)) {
|
||||
m_pendingEffects << effect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EffectsModel::loadPluginEffects(const KConfigGroup &kwinConfig)
|
||||
{
|
||||
const auto pluginEffects = KPluginMetaData::findPlugins(QStringLiteral("kwin/effects/plugins"));
|
||||
for (const KPluginMetaData &pluginEffect : pluginEffects) {
|
||||
if (!pluginEffect.isValid()) {
|
||||
continue;
|
||||
}
|
||||
EffectData effect;
|
||||
effect.name = pluginEffect.name();
|
||||
effect.description = pluginEffect.description();
|
||||
effect.license = pluginEffect.license();
|
||||
effect.version = pluginEffect.version();
|
||||
effect.untranslatedCategory = pluginEffect.category();
|
||||
effect.category = translatedCategory(pluginEffect.category());
|
||||
effect.serviceName = pluginEffect.pluginId();
|
||||
effect.iconName = pluginEffect.iconName();
|
||||
effect.enabledByDefault = pluginEffect.isEnabledByDefault();
|
||||
effect.supported = true;
|
||||
effect.enabledByDefaultFunction = false;
|
||||
effect.internal = false;
|
||||
effect.configModule = pluginEffect.value(QStringLiteral("X-KDE-ConfigModule"));
|
||||
|
||||
for (int i = 0; i < pluginEffect.authors().count(); ++i) {
|
||||
effect.authorName.append(pluginEffect.authors().at(i).name());
|
||||
effect.authorEmail.append(pluginEffect.authors().at(i).emailAddress());
|
||||
if (i + 1 < pluginEffect.authors().count()) {
|
||||
effect.authorName.append(", ");
|
||||
effect.authorEmail.append(", ");
|
||||
}
|
||||
}
|
||||
|
||||
if (pluginEffect.rawData().contains("org.kde.kwin.effect")) {
|
||||
const QJsonObject d(pluginEffect.rawData().value("org.kde.kwin.effect").toObject());
|
||||
effect.exclusiveGroup = d.value("exclusiveGroup").toString();
|
||||
effect.video = QUrl::fromUserInput(d.value("video").toString());
|
||||
effect.enabledByDefaultFunction = d.value("enabledByDefaultMethod").toBool();
|
||||
}
|
||||
|
||||
effect.website = QUrl(pluginEffect.website());
|
||||
|
||||
const QString enabledKey = QStringLiteral("%1Enabled").arg(effect.serviceName);
|
||||
if (kwinConfig.hasKey(enabledKey)) {
|
||||
effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", effect.enabledByDefault));
|
||||
} else if (effect.enabledByDefaultFunction) {
|
||||
effect.status = Status::EnabledUndeterminded;
|
||||
} else {
|
||||
effect.status = effectStatus(effect.enabledByDefault);
|
||||
}
|
||||
|
||||
effect.originalStatus = effect.status;
|
||||
|
||||
if (shouldStore(effect)) {
|
||||
m_pendingEffects << effect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EffectsModel::load(LoadOptions options)
|
||||
{
|
||||
KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), QStringLiteral("Plugins"));
|
||||
|
||||
m_pendingEffects.clear();
|
||||
loadBuiltInEffects(kwinConfig);
|
||||
loadJavascriptEffects(kwinConfig);
|
||||
loadPluginEffects(kwinConfig);
|
||||
|
||||
std::sort(m_pendingEffects.begin(), m_pendingEffects.end(),
|
||||
[](const EffectData &a, const EffectData &b) {
|
||||
if (a.category == b.category) {
|
||||
if (a.exclusiveGroup == b.exclusiveGroup) {
|
||||
return a.name < b.name;
|
||||
}
|
||||
return a.exclusiveGroup < b.exclusiveGroup;
|
||||
}
|
||||
return a.category < b.category;
|
||||
});
|
||||
|
||||
auto commit = [this, options] {
|
||||
if (options == LoadOptions::KeepDirty) {
|
||||
for (const EffectData &oldEffect : std::as_const(m_effects)) {
|
||||
if (!oldEffect.changed) {
|
||||
continue;
|
||||
}
|
||||
auto effectIt = std::find_if(m_pendingEffects.begin(), m_pendingEffects.end(),
|
||||
[oldEffect](const EffectData &data) {
|
||||
return data.serviceName == oldEffect.serviceName;
|
||||
});
|
||||
if (effectIt == m_pendingEffects.end()) {
|
||||
continue;
|
||||
}
|
||||
effectIt->status = oldEffect.status;
|
||||
effectIt->changed = effectIt->status != effectIt->originalStatus;
|
||||
}
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
m_effects = m_pendingEffects;
|
||||
m_pendingEffects.clear();
|
||||
endResetModel();
|
||||
|
||||
Q_EMIT loaded();
|
||||
};
|
||||
|
||||
OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"),
|
||||
QStringLiteral("/Effects"),
|
||||
QDBusConnection::sessionBus());
|
||||
|
||||
if (interface.isValid()) {
|
||||
QStringList effectNames;
|
||||
effectNames.reserve(m_pendingEffects.count());
|
||||
for (const EffectData &data : std::as_const(m_pendingEffects)) {
|
||||
effectNames.append(data.serviceName);
|
||||
}
|
||||
|
||||
const int serial = ++m_lastSerial;
|
||||
|
||||
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(interface.areEffectsSupported(effectNames), this);
|
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [=, this](QDBusPendingCallWatcher *self) {
|
||||
self->deleteLater();
|
||||
|
||||
if (m_lastSerial != serial) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QDBusPendingReply<QList<bool>> reply = *self;
|
||||
if (reply.isError()) {
|
||||
commit();
|
||||
return;
|
||||
}
|
||||
|
||||
const QList<bool> supportedValues = reply.value();
|
||||
if (supportedValues.count() != effectNames.count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < effectNames.size(); ++i) {
|
||||
const bool supported = supportedValues.at(i);
|
||||
const QString effectName = effectNames.at(i);
|
||||
|
||||
auto it = std::find_if(m_pendingEffects.begin(), m_pendingEffects.end(),
|
||||
[effectName](const EffectData &data) {
|
||||
return data.serviceName == effectName;
|
||||
});
|
||||
if (it == m_pendingEffects.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((*it).supported != supported) {
|
||||
(*it).supported = supported;
|
||||
}
|
||||
}
|
||||
|
||||
commit();
|
||||
});
|
||||
} else {
|
||||
commit();
|
||||
}
|
||||
}
|
||||
|
||||
void EffectsModel::updateEffectStatus(const QModelIndex &rowIndex, Status effectState)
|
||||
{
|
||||
setData(rowIndex, static_cast<int>(effectState), StatusRole);
|
||||
}
|
||||
|
||||
void EffectsModel::save()
|
||||
{
|
||||
KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), QStringLiteral("Plugins"));
|
||||
|
||||
QList<EffectData> dirtyEffects;
|
||||
|
||||
for (EffectData &effect : m_effects) {
|
||||
if (!effect.changed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
effect.changed = false;
|
||||
effect.originalStatus = effect.status;
|
||||
|
||||
const QString key = effect.serviceName + QStringLiteral("Enabled");
|
||||
const bool shouldEnable = (effect.status != Status::Disabled);
|
||||
const bool restoreToDefault = effect.enabledByDefaultFunction
|
||||
? effect.status == Status::EnabledUndeterminded
|
||||
: shouldEnable == effect.enabledByDefault;
|
||||
if (restoreToDefault) {
|
||||
kwinConfig.deleteEntry(key);
|
||||
} else {
|
||||
kwinConfig.writeEntry(key, shouldEnable);
|
||||
}
|
||||
|
||||
dirtyEffects.append(effect);
|
||||
}
|
||||
|
||||
if (dirtyEffects.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
kwinConfig.sync();
|
||||
|
||||
OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"),
|
||||
QStringLiteral("/Effects"),
|
||||
QDBusConnection::sessionBus());
|
||||
|
||||
if (!interface.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unload effects first, it's need to ensure that switching between mutually exclusive
|
||||
// effects works as expected, for example so global shortcuts are handed over, etc.
|
||||
auto split = std::partition(dirtyEffects.begin(), dirtyEffects.end(), [](const EffectData &data) {
|
||||
return data.status == Status::Disabled;
|
||||
});
|
||||
|
||||
for (auto it = dirtyEffects.begin(); it != split; ++it) {
|
||||
interface.unloadEffect(it->serviceName);
|
||||
}
|
||||
|
||||
for (auto it = split; it != dirtyEffects.end(); ++it) {
|
||||
interface.loadEffect(it->serviceName);
|
||||
}
|
||||
}
|
||||
|
||||
void EffectsModel::defaults()
|
||||
{
|
||||
for (int i = 0; i < m_effects.count(); ++i) {
|
||||
const auto &effect = m_effects.at(i);
|
||||
if (effect.enabledByDefaultFunction && effect.status != Status::EnabledUndeterminded) {
|
||||
updateEffectStatus(index(i, 0), Status::EnabledUndeterminded);
|
||||
} else if (static_cast<bool>(effect.status) != effect.enabledByDefault) {
|
||||
updateEffectStatus(index(i, 0), effect.enabledByDefault ? Status::Enabled : Status::Disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool EffectsModel::isDefaults() const
|
||||
{
|
||||
return std::all_of(m_effects.constBegin(), m_effects.constEnd(), [](const EffectData &effect) {
|
||||
if (effect.enabledByDefaultFunction && effect.status != Status::EnabledUndeterminded) {
|
||||
return false;
|
||||
}
|
||||
if (static_cast<bool>(effect.status) != effect.enabledByDefault) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool EffectsModel::needsSave() const
|
||||
{
|
||||
return std::any_of(m_effects.constBegin(), m_effects.constEnd(),
|
||||
[](const EffectData &data) {
|
||||
return data.changed;
|
||||
});
|
||||
}
|
||||
|
||||
QModelIndex EffectsModel::findByPluginId(const QString &pluginId) const
|
||||
{
|
||||
auto it = std::find_if(m_effects.constBegin(), m_effects.constEnd(),
|
||||
[pluginId](const EffectData &data) {
|
||||
return data.serviceName == pluginId;
|
||||
});
|
||||
if (it == m_effects.constEnd()) {
|
||||
return {};
|
||||
}
|
||||
return index(std::distance(m_effects.constBegin(), it), 0);
|
||||
}
|
||||
|
||||
void EffectsModel::requestConfigure(const QModelIndex &index, QWindow *transientParent)
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const EffectData &effect = m_effects.at(index.row());
|
||||
Q_ASSERT(!effect.configModule.isEmpty());
|
||||
|
||||
KCMultiDialog *dialog = new KCMultiDialog();
|
||||
dialog->addModule(KPluginMetaData(QStringLiteral("kwin/effects/configs/") + effect.configModule), effect.configArgs);
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->winId();
|
||||
dialog->windowHandle()->setTransientParent(transientParent);
|
||||
dialog->show();
|
||||
}
|
||||
|
||||
bool EffectsModel::shouldStore(const EffectData &data) const
|
||||
{
|
||||
return !data.internal;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#include "moc_effectsmodel.cpp"
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
KWin - the KDE window manager
|
||||
This file is part of the KDE project.
|
||||
|
||||
SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas <kok3rs@gmail.com>
|
||||
SPDX-FileCopyrightText: 2018 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <kwin_export.h>
|
||||
|
||||
#include <KSharedConfig>
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QWindow>
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
|
||||
class KWIN_EXPORT EffectsModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* This enum type is used to specify data roles.
|
||||
*/
|
||||
enum AdditionalRoles {
|
||||
/**
|
||||
* The user-friendly name of the effect.
|
||||
*/
|
||||
NameRole = Qt::UserRole + 1,
|
||||
/**
|
||||
* The description of the effect.
|
||||
*/
|
||||
DescriptionRole,
|
||||
/**
|
||||
* The name of the effect's author. If there are several authors, they
|
||||
* will be comma separated.
|
||||
*/
|
||||
AuthorNameRole,
|
||||
/**
|
||||
* The email of the effect's author. If there are several authors, the
|
||||
* emails will be comma separated.
|
||||
*/
|
||||
AuthorEmailRole,
|
||||
/**
|
||||
* The license of the effect.
|
||||
*/
|
||||
LicenseRole,
|
||||
/**
|
||||
* The version of the effect.
|
||||
*/
|
||||
VersionRole,
|
||||
/**
|
||||
* The category of the effect.
|
||||
*/
|
||||
CategoryRole,
|
||||
/**
|
||||
* The service name(plugin name) of the effect.
|
||||
*/
|
||||
ServiceNameRole,
|
||||
/**
|
||||
* The icon name of the effect.
|
||||
*/
|
||||
IconNameRole,
|
||||
/**
|
||||
* Whether the effect is enabled or disabled.
|
||||
*/
|
||||
StatusRole,
|
||||
/**
|
||||
* Link to a video demonstration of the effect.
|
||||
*/
|
||||
VideoRole,
|
||||
/**
|
||||
* Link to the home page of the effect.
|
||||
*/
|
||||
WebsiteRole,
|
||||
/**
|
||||
* Whether the effect is supported.
|
||||
*/
|
||||
SupportedRole,
|
||||
/**
|
||||
* The exclusive group of the effect.
|
||||
*/
|
||||
ExclusiveRole,
|
||||
/**
|
||||
* Whether the effect is internal.
|
||||
*/
|
||||
InternalRole,
|
||||
/**
|
||||
* Whether the effect has a KCM.
|
||||
*/
|
||||
ConfigurableRole,
|
||||
/**
|
||||
* Whether the effect is enabled by default.
|
||||
*/
|
||||
EnabledByDefaultRole,
|
||||
/**
|
||||
* Id of the effect's config module, empty if the effect has no config.
|
||||
*/
|
||||
ConfigModuleRole,
|
||||
/**
|
||||
* Whether the effect has a function to determine if the effect is enabled by default.
|
||||
*/
|
||||
EnabledByDefaultFunctionRole,
|
||||
};
|
||||
|
||||
/**
|
||||
* This enum type is used to specify the status of a given effect.
|
||||
*/
|
||||
enum class Status {
|
||||
/**
|
||||
* The effect is disabled.
|
||||
*/
|
||||
Disabled = Qt::Unchecked,
|
||||
/**
|
||||
* An enable function is used to determine whether the effect is enabled.
|
||||
* For example, such function can be useful to disable the blur effect
|
||||
* when running in a virtual machine.
|
||||
*/
|
||||
EnabledUndeterminded = Qt::PartiallyChecked,
|
||||
/**
|
||||
* The effect is enabled.
|
||||
*/
|
||||
Enabled = Qt::Checked
|
||||
};
|
||||
|
||||
explicit EffectsModel(QObject *parent = nullptr);
|
||||
|
||||
// Reimplemented from QAbstractItemModel.
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override;
|
||||
QModelIndex parent(const QModelIndex &child) const override;
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
int columnCount(const QModelIndex &parent = {}) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
|
||||
|
||||
/**
|
||||
* Changes the status of a given effect.
|
||||
*
|
||||
* @param rowIndex An effect represented by the given index.
|
||||
* @param effectState The new state.
|
||||
* @note In order to actually apply the change, you have to call save().
|
||||
*/
|
||||
void updateEffectStatus(const QModelIndex &rowIndex, Status effectState);
|
||||
|
||||
/**
|
||||
* This enum type is used to specify load options.
|
||||
*/
|
||||
enum class LoadOptions {
|
||||
None,
|
||||
/**
|
||||
* Do not discard unsaved changes when reloading the model.
|
||||
*/
|
||||
KeepDirty
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads effects.
|
||||
*
|
||||
* You have to call this method in order to populate the model.
|
||||
*/
|
||||
void load(LoadOptions options = LoadOptions::None);
|
||||
|
||||
/**
|
||||
* Saves status of each modified effect.
|
||||
*/
|
||||
void save();
|
||||
|
||||
/**
|
||||
* Resets the status of each effect to the default state.
|
||||
*
|
||||
* @note In order to actually apply the change, you have to call save().
|
||||
*/
|
||||
void defaults();
|
||||
|
||||
/**
|
||||
* Whether the status of each effect is its default state.
|
||||
*/
|
||||
bool isDefaults() const;
|
||||
|
||||
/**
|
||||
* Whether the model has unsaved changes.
|
||||
*/
|
||||
bool needsSave() const;
|
||||
|
||||
/**
|
||||
* Finds an effect with the given plugin id.
|
||||
*/
|
||||
QModelIndex findByPluginId(const QString &pluginId) const;
|
||||
|
||||
/**
|
||||
* Shows a configuration dialog for a given effect.
|
||||
*
|
||||
* @param index An effect represented by the given index.
|
||||
* @param transientParent The transient parent of the configuration dialog.
|
||||
*/
|
||||
void requestConfigure(const QModelIndex &index, QWindow *transientParent);
|
||||
|
||||
Q_SIGNALS:
|
||||
/**
|
||||
* This signal is emitted when the model is loaded or reloaded.
|
||||
*
|
||||
* @see load
|
||||
*/
|
||||
void loaded();
|
||||
|
||||
protected:
|
||||
struct EffectData
|
||||
{
|
||||
QString name;
|
||||
QString description;
|
||||
QString authorName;
|
||||
QString authorEmail;
|
||||
QString license;
|
||||
QString version;
|
||||
QString untranslatedCategory;
|
||||
QString category;
|
||||
QString serviceName;
|
||||
QString iconName;
|
||||
Status status;
|
||||
Status originalStatus;
|
||||
bool enabledByDefault;
|
||||
bool enabledByDefaultFunction;
|
||||
QUrl video;
|
||||
QUrl website;
|
||||
bool supported;
|
||||
QString exclusiveGroup;
|
||||
bool internal;
|
||||
bool changed = false;
|
||||
QString configModule;
|
||||
QVariantList configArgs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the given effect should be stored in the model.
|
||||
*
|
||||
* @param data The effect.
|
||||
* @returns @c true if the effect should be stored, otherwise @c false.
|
||||
*/
|
||||
virtual bool shouldStore(const EffectData &data) const;
|
||||
|
||||
private:
|
||||
void loadBuiltInEffects(const KConfigGroup &kwinConfig);
|
||||
void loadJavascriptEffects(const KConfigGroup &kwinConfig);
|
||||
void loadPluginEffects(const KConfigGroup &kwinConfig);
|
||||
|
||||
QList<EffectData> m_effects;
|
||||
QList<EffectData> m_pendingEffects;
|
||||
int m_lastSerial = -1;
|
||||
|
||||
Q_DISABLE_COPY(EffectsModel)
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
KWin - the KDE window manager
|
||||
This file is part of the KDE project.
|
||||
|
||||
SPDX-FileCopyrightText: 2013 Martin Gräßlin <mgraesslin@kde.org>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
#include "genericscriptedconfig.h"
|
||||
|
||||
#include "config-kwin.h"
|
||||
|
||||
#include <kwineffects_interface.h>
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <KLocalizedTranslator>
|
||||
#include <kconfigloader.h>
|
||||
|
||||
#include <QFile>
|
||||
#include <QLabel>
|
||||
#include <QStandardPaths>
|
||||
#include <QUiLoader>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
|
||||
QObject *GenericScriptedConfigFactory::create(const char *iface, QWidget *parentWidget, QObject *parent, const QVariantList &args)
|
||||
{
|
||||
if (qstrcmp(iface, "KCModule") == 0) {
|
||||
if (args.count() < 2) {
|
||||
qWarning() << Q_FUNC_INFO << "expects two arguments (plugin id, package type)";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const QString pluginId = args.at(0).toString();
|
||||
const QString packageType = args.at(1).toString();
|
||||
|
||||
if (packageType == QLatin1StringView("KWin/Effect")) {
|
||||
return new ScriptedEffectConfig(pluginId, parentWidget, args);
|
||||
} else if (packageType == QLatin1StringView("KWin/Script")) {
|
||||
return new ScriptingConfig(pluginId, parentWidget, args);
|
||||
} else {
|
||||
qWarning() << Q_FUNC_INFO << "got unknown package type:" << packageType;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GenericScriptedConfig::GenericScriptedConfig(const QString &keyword, QWidget *parent, const QVariantList &args)
|
||||
: KCModule(parent, KPluginMetaData())
|
||||
, m_packageName(keyword)
|
||||
, m_translator(new KLocalizedTranslator(this))
|
||||
{
|
||||
QCoreApplication::instance()->installTranslator(m_translator);
|
||||
}
|
||||
|
||||
GenericScriptedConfig::~GenericScriptedConfig()
|
||||
{
|
||||
}
|
||||
|
||||
void GenericScriptedConfig::createUi()
|
||||
{
|
||||
QVBoxLayout *layout = new QVBoxLayout(widget());
|
||||
|
||||
const QString packageRoot = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
|
||||
QLatin1String("kwin/") + typeName() + QLatin1Char('/') + m_packageName,
|
||||
QStandardPaths::LocateDirectory);
|
||||
if (packageRoot.isEmpty()) {
|
||||
layout->addWidget(new QLabel(i18nc("Error message", "Could not locate package metadata")));
|
||||
return;
|
||||
}
|
||||
|
||||
const KPluginMetaData metaData = KPluginMetaData::fromJsonFile(packageRoot + QLatin1String("/metadata.json"));
|
||||
if (!metaData.isValid()) {
|
||||
layout->addWidget(new QLabel(i18nc("Required file does not exist", "%1 does not contain a valid metadata.json file", qPrintable(packageRoot))));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString kconfigXTFile = packageRoot + QLatin1String("/contents/config/main.xml");
|
||||
if (!QFileInfo::exists(kconfigXTFile)) {
|
||||
layout->addWidget(new QLabel(i18nc("Required file does not exist", "%1 does not exist", qPrintable(kconfigXTFile))));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString uiPath = packageRoot + QLatin1String("/contents/ui/config.ui");
|
||||
if (!QFileInfo::exists(uiPath)) {
|
||||
layout->addWidget(new QLabel(i18nc("Required file does not exist", "%1 does not exist", qPrintable(uiPath))));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString localePath = packageRoot + QLatin1String("/contents/locale");
|
||||
if (QFileInfo::exists(localePath)) {
|
||||
KLocalizedString::addDomainLocaleDir(metaData.value("X-KWin-Config-TranslationDomain").toUtf8(), localePath);
|
||||
}
|
||||
|
||||
QFile xmlFile(kconfigXTFile);
|
||||
KConfigGroup cg = configGroup();
|
||||
KConfigLoader *configLoader = new KConfigLoader(cg, &xmlFile, this);
|
||||
// load the ui file
|
||||
QUiLoader *loader = new QUiLoader(this);
|
||||
loader->setLanguageChangeEnabled(true);
|
||||
QFile uiFile(uiPath);
|
||||
m_translator->setTranslationDomain(metaData.value("X-KWin-Config-TranslationDomain"));
|
||||
|
||||
uiFile.open(QFile::ReadOnly);
|
||||
QWidget *customConfigForm = loader->load(&uiFile, widget());
|
||||
m_translator->addContextToMonitor(customConfigForm->objectName());
|
||||
uiFile.close();
|
||||
|
||||
// send a custom event to the translator to retranslate using our translator
|
||||
QEvent le(QEvent::LanguageChange);
|
||||
QCoreApplication::sendEvent(customConfigForm, &le);
|
||||
|
||||
layout->addWidget(customConfigForm);
|
||||
addConfig(configLoader, customConfigForm);
|
||||
}
|
||||
|
||||
void GenericScriptedConfig::save()
|
||||
{
|
||||
KCModule::save();
|
||||
reload();
|
||||
}
|
||||
|
||||
void GenericScriptedConfig::reload()
|
||||
{
|
||||
}
|
||||
|
||||
ScriptedEffectConfig::ScriptedEffectConfig(const QString &keyword, QWidget *parent, const QVariantList &args)
|
||||
: GenericScriptedConfig(keyword, parent, args)
|
||||
{
|
||||
createUi();
|
||||
}
|
||||
|
||||
ScriptedEffectConfig::~ScriptedEffectConfig()
|
||||
{
|
||||
}
|
||||
|
||||
QString ScriptedEffectConfig::typeName() const
|
||||
{
|
||||
return QStringLiteral("effects");
|
||||
}
|
||||
|
||||
KConfigGroup ScriptedEffectConfig::configGroup()
|
||||
{
|
||||
return KSharedConfig::openConfig(KWIN_CONFIG)->group(QLatin1String("Effect-") + packageName());
|
||||
}
|
||||
|
||||
void ScriptedEffectConfig::reload()
|
||||
{
|
||||
OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"),
|
||||
QStringLiteral("/Effects"),
|
||||
QDBusConnection::sessionBus());
|
||||
interface.reconfigureEffect(packageName());
|
||||
}
|
||||
|
||||
ScriptingConfig::ScriptingConfig(const QString &keyword, QWidget *parent, const QVariantList &args)
|
||||
: GenericScriptedConfig(keyword, parent, args)
|
||||
{
|
||||
createUi();
|
||||
}
|
||||
|
||||
ScriptingConfig::~ScriptingConfig()
|
||||
{
|
||||
}
|
||||
|
||||
KConfigGroup ScriptingConfig::configGroup()
|
||||
{
|
||||
return KSharedConfig::openConfig(KWIN_CONFIG)->group(QLatin1String("Script-") + packageName());
|
||||
}
|
||||
|
||||
QString ScriptingConfig::typeName() const
|
||||
{
|
||||
return QStringLiteral("scripts");
|
||||
}
|
||||
|
||||
void ScriptingConfig::reload()
|
||||
{
|
||||
// TODO: what to call
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#include "moc_genericscriptedconfig.cpp"
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
KWin - the KDE window manager
|
||||
This file is part of the KDE project.
|
||||
|
||||
SPDX-FileCopyrightText: 2013 Martin Gräßlin <mgraesslin@kde.org>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <KCModule>
|
||||
#include <KConfigGroup>
|
||||
#include <KPluginFactory>
|
||||
|
||||
class KLocalizedTranslator;
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
|
||||
class GenericScriptedConfigFactory : public KPluginFactory
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.kde.KPluginFactory" FILE "genericscriptedconfig.json")
|
||||
Q_INTERFACES(KPluginFactory)
|
||||
|
||||
protected:
|
||||
QObject *create(const char *iface, QWidget *parentWidget, QObject *parent, const QVariantList &args) override;
|
||||
};
|
||||
|
||||
class GenericScriptedConfig : public KCModule
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
GenericScriptedConfig(const QString &keyword, QWidget *parent, const QVariantList &args);
|
||||
~GenericScriptedConfig() override;
|
||||
|
||||
public Q_SLOTS:
|
||||
void save() override;
|
||||
|
||||
protected:
|
||||
const QString &packageName() const;
|
||||
void createUi();
|
||||
virtual QString typeName() const = 0;
|
||||
virtual KConfigGroup configGroup() = 0;
|
||||
virtual void reload();
|
||||
|
||||
private:
|
||||
QString m_packageName;
|
||||
KLocalizedTranslator *m_translator;
|
||||
};
|
||||
|
||||
class ScriptedEffectConfig : public GenericScriptedConfig
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ScriptedEffectConfig(const QString &keyword, QWidget *parent, const QVariantList &args);
|
||||
~ScriptedEffectConfig() override;
|
||||
|
||||
protected:
|
||||
QString typeName() const override;
|
||||
KConfigGroup configGroup() override;
|
||||
void reload() override;
|
||||
};
|
||||
|
||||
class ScriptingConfig : public GenericScriptedConfig
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ScriptingConfig(const QString &keyword, QWidget *parent, const QVariantList &args);
|
||||
~ScriptingConfig() override;
|
||||
|
||||
protected:
|
||||
QString typeName() const override;
|
||||
KConfigGroup configGroup() override;
|
||||
void reload() override;
|
||||
};
|
||||
|
||||
inline const QString &GenericScriptedConfig::packageName() const
|
||||
{
|
||||
return m_packageName;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"Type": "Service",
|
||||
"X-KDE-Library": "kcm_kwin4_genericscripted"
|
||||
}
|
||||
Reference in New Issue
Block a user