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"