Warning, file /plasma/plasma-workspace/kcms/soundtheme/kcm_soundtheme.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2023 Ismael Asensio <isma.af@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "kcm_soundtheme.h"
0008 
0009 #include "kcm_soundtheme_debug.h"
0010 #include "soundthemedata.h"
0011 
0012 #include <canberra.h>
0013 
0014 #include <QCollator>
0015 #include <QDir>
0016 
0017 #include <KConfig>
0018 #include <KConfigGroup>
0019 #include <KLocalizedString>
0020 #include <KPluginFactory>
0021 
0022 using namespace Qt::StringLiterals;
0023 
0024 K_PLUGIN_FACTORY_WITH_JSON(KCMSoundThemeFactory, "kcm_soundtheme.json", registerPlugin<KCMSoundTheme>(); registerPlugin<SoundThemeData>();)
0025 
0026 constexpr QLatin1String FALLBACK_THEME = QLatin1String("freedesktop");
0027 
0028 KCMSoundTheme::KCMSoundTheme(QObject *parent, const KPluginMetaData &data)
0029     : KQuickManagedConfigModule(parent, data)
0030     , m_data(new SoundThemeData(this))
0031 {
0032     registerSettings(m_data->settings());
0033 
0034     qmlRegisterUncreatableType<SoundThemeSettings *>("org.kde.private.kcms.soundtheme", 1, 0, "Settings", QStringLiteral("SoundTheme settings"));
0035 
0036     connect(m_data->settings(), &SoundThemeSettings::themeChanged, this, &KCMSoundTheme::themeChanged);
0037     connect(m_data->settings(), &SoundThemeSettings::soundsEnabledChanged, this, &KCMSoundTheme::cancelSound);
0038 }
0039 
0040 KCMSoundTheme::~KCMSoundTheme()
0041 {
0042     if (m_canberraContext) {
0043         ca_context_destroy(m_canberraContext);
0044     }
0045 }
0046 
0047 SoundThemeSettings *KCMSoundTheme::settings() const
0048 {
0049     return m_data->settings();
0050 }
0051 
0052 int KCMSoundTheme::currentIndex() const
0053 {
0054     return indexOf(m_data->settings()->theme());
0055 }
0056 
0057 int KCMSoundTheme::indexOf(const QString &themeId) const
0058 {
0059     for (int row = 0; row < m_themes.count(); row++) {
0060         const auto &theme = m_themes.at(row);
0061         if (theme->id == themeId) {
0062             return row;
0063         }
0064     }
0065     return -1;
0066 }
0067 
0068 QString KCMSoundTheme::nameFor(const QString &themeId) const
0069 {
0070     const int index = indexOf(themeId);
0071     if (index < 0) {
0072         return themeId;
0073     }
0074     return m_themes.at(index)->name;
0075 }
0076 
0077 void KCMSoundTheme::load()
0078 {
0079     KQuickManagedConfigModule::load();
0080     loadThemes();
0081 }
0082 
0083 void KCMSoundTheme::loadThemes()
0084 {
0085     // Spec-compliant themes are stored in any of the standard locations `.../share/sounds/<themeId>`
0086     // and must contain a descriptive `index.theme` file. The properties of the themes can be extended
0087     // in the user-local paths so we need to cascade their description files
0088     // Reference: http://0pointer.de/public/sound-theme-spec.html
0089 
0090     m_themes.clear();
0091 
0092     const QStringList soundLocations =
0093         QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("sounds"), QStandardPaths::LocateDirectory);
0094 
0095     QStringList themeIds;
0096     for (const QString &location : soundLocations) {
0097         for (const QString &dirName : QDir(location).entryList({}, QDir::AllDirs | QDir::Readable | QDir::NoDotAndDotDot)) {
0098             if (themeIds.contains(dirName)) {
0099                 continue;
0100             }
0101             themeIds << dirName;
0102 
0103             ThemeInfo *theme = new ThemeInfo(dirName, this);
0104             if (!theme->isValid || theme->isHidden) {
0105                 delete theme;
0106                 continue;
0107             }
0108             // The fallback "freedesktop" theme identifies itself as "Default" with no comment nor translations
0109             // which can get confused with the system's default theme
0110             if (theme->id == FALLBACK_THEME) {
0111                 theme->name = i18nc("Name of the fallback \"freedesktop\" sound theme", "FreeDesktop");
0112                 theme->comment = i18n("Fallback sound theme from freedesktop.org");
0113             }
0114             m_themes << theme;
0115         }
0116     }
0117 
0118     QCollator collator;
0119     // Sort by theme name, but leave "freedesktop" default at the last position
0120     std::sort(m_themes.begin(), m_themes.end(), [&collator](auto *a, auto *b) {
0121         if (a->id == FALLBACK_THEME) {
0122             return false;
0123         }
0124         if (b->id == FALLBACK_THEME) {
0125             return true;
0126         }
0127         return collator.compare(a->name, b->name) < 0;
0128     });
0129 
0130     Q_EMIT themesLoaded();
0131     Q_EMIT themeChanged();
0132 }
0133 
0134 ca_context *KCMSoundTheme::canberraContext()
0135 {
0136     if (!m_canberraContext) {
0137         int ret = ca_context_create(&m_canberraContext);
0138         if (ret != CA_SUCCESS) {
0139             qCWarning(KCM_SOUNDTHEME) << "Failed to initialize canberra context for audio notification:" << ca_strerror(ret);
0140             m_canberraContext = nullptr;
0141             return nullptr;
0142         }
0143 
0144         // clang-format off
0145         ret = ca_context_change_props(m_canberraContext,
0146                                       CA_PROP_APPLICATION_NAME, qUtf8Printable(metaData().name()),
0147                                       CA_PROP_APPLICATION_ID, qUtf8Printable(metaData().pluginId()),
0148                                       CA_PROP_APPLICATION_ICON_NAME, qUtf8Printable(metaData().iconName()),
0149                                       nullptr);
0150         // clang-format on
0151         if (ret != CA_SUCCESS) {
0152             qCWarning(KCM_SOUNDTHEME) << "Failed to set application properties on canberra context for audio notification:" << ca_strerror(ret);
0153         }
0154     }
0155 
0156     return m_canberraContext;
0157 }
0158 
0159 int KCMSoundTheme::playSound(const QString &themeId, const QStringList &soundList)
0160 {
0161     ca_proplist *props = nullptr;
0162     ca_proplist_create(&props);
0163     ca_proplist_sets(props, CA_PROP_CANBERRA_XDG_THEME_NAME, themeId.toLatin1().constData());
0164     ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "volatile");
0165 
0166     // We don't want several previews playing at the same time
0167     ca_context_cancel(canberraContext(), 0);
0168 
0169     int result = CA_SUCCESS;
0170     for (const QString &soundName : soundList) {
0171         ca_proplist_sets(props, CA_PROP_EVENT_ID, soundName.toLatin1().constData());
0172         result = ca_context_play_full(canberraContext(), 0, props, &ca_finish_callback, this);
0173         qCDebug(KCM_SOUNDTHEME) << "Try playing sound" << soundName << "for theme" << themeId << ":" << ca_strerror(result);
0174         if (result == CA_SUCCESS) {
0175             m_playingTheme = themeId;
0176             m_playingSound = soundName;
0177             Q_EMIT playingChanged();
0178             break;
0179         }
0180     }
0181 
0182     ca_proplist_destroy(props);
0183 
0184     return result;
0185 }
0186 
0187 void KCMSoundTheme::cancelSound()
0188 {
0189     ca_context_cancel(canberraContext(), 0);
0190 }
0191 
0192 QString KCMSoundTheme::errorString(int errorCode)
0193 {
0194     return QString::fromUtf8(ca_strerror(errorCode));
0195 }
0196 
0197 void KCMSoundTheme::ca_finish_callback(ca_context *c, uint32_t id, int error_code, void *userdata)
0198 {
0199     Q_UNUSED(c);
0200     Q_UNUSED(id);
0201     Q_UNUSED(error_code);
0202     QMetaObject::invokeMethod(static_cast<KCMSoundTheme *>(userdata), "onPlayingFinished");
0203 }
0204 
0205 void KCMSoundTheme::onPlayingFinished()
0206 {
0207     m_playingTheme = QString();
0208     m_playingSound = QString();
0209     Q_EMIT playingChanged();
0210 }
0211 
0212 ThemeInfo::ThemeInfo(const QString &themeId, QObject *parent)
0213     : QObject(parent)
0214 {
0215     const QStringList themeInfoSources = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/%1/index.theme").arg(themeId));
0216 
0217     if (themeInfoSources.isEmpty()) {
0218         return;
0219     }
0220 
0221     KConfig config = KConfig();
0222     config.addConfigSources(themeInfoSources);
0223 
0224     KConfigGroup themeGroup = config.group(u"Sound Theme"_s);
0225     if (!themeGroup.exists()) {
0226         return;
0227     }
0228 
0229     id = themeId;
0230     name = themeGroup.readEntry("Name", themeId);
0231     comment = themeGroup.readEntry("Comment", {});
0232     inherits = themeGroup.readEntry("Inherits", QStringList());
0233     directories = themeGroup.readEntry("Directories", QStringList());
0234     isHidden = themeGroup.readEntry("Hidden", false);
0235     example = themeGroup.readEntry("Example", {});
0236 
0237     isValid = true;
0238 }
0239 
0240 #include "kcm_soundtheme.moc"