/* SPDX-FileCopyrightText: 2023 Ismael Asensio SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "kcm_soundtheme.h" #include "kcm_soundtheme_debug.h" #include "soundthemedata.h" #include #include #include #include #include #include #include using namespace Qt::StringLiterals; K_PLUGIN_FACTORY_WITH_JSON(KCMSoundThemeFactory, "kcm_soundtheme.json", registerPlugin(); registerPlugin();) constexpr QLatin1String FALLBACK_THEME = QLatin1String("freedesktop"); KCMSoundTheme::KCMSoundTheme(QObject *parent, const KPluginMetaData &data) : KQuickManagedConfigModule(parent, data) , m_data(new SoundThemeData(this)) { registerSettings(m_data->settings()); qmlRegisterUncreatableType("org.kde.private.kcms.soundtheme", 1, 0, "Settings", QStringLiteral("SoundTheme settings")); connect(m_data->settings(), &SoundThemeSettings::themeChanged, this, &KCMSoundTheme::themeChanged); connect(m_data->settings(), &SoundThemeSettings::soundsEnabledChanged, this, &KCMSoundTheme::cancelSound); } KCMSoundTheme::~KCMSoundTheme() { if (m_canberraContext) { ca_context_destroy(m_canberraContext); } } SoundThemeSettings *KCMSoundTheme::settings() const { return m_data->settings(); } int KCMSoundTheme::currentIndex() const { return indexOf(m_data->settings()->theme()); } int KCMSoundTheme::indexOf(const QString &themeId) const { for (int row = 0; row < m_themes.count(); row++) { const auto &theme = m_themes.at(row); if (theme->id == themeId) { return row; } } return -1; } QString KCMSoundTheme::nameFor(const QString &themeId) const { const int index = indexOf(themeId); if (index < 0) { return themeId; } return m_themes.at(index)->name; } void KCMSoundTheme::load() { KQuickManagedConfigModule::load(); loadThemes(); } void KCMSoundTheme::loadThemes() { // Spec-compliant themes are stored in any of the standard locations `.../share/sounds/` // and must contain a descriptive `index.theme` file. The properties of the themes can be extended // in the user-local paths so we need to cascade their description files // Reference: http://0pointer.de/public/sound-theme-spec.html m_themes.clear(); const QStringList soundLocations = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("sounds"), QStandardPaths::LocateDirectory); QStringList themeIds; for (const QString &location : soundLocations) { for (const QString &dirName : QDir(location).entryList({}, QDir::AllDirs | QDir::Readable | QDir::NoDotAndDotDot)) { if (themeIds.contains(dirName)) { continue; } themeIds << dirName; ThemeInfo *theme = new ThemeInfo(dirName, this); if (!theme->isValid || theme->isHidden) { delete theme; continue; } // The fallback "freedesktop" theme identifies itself as "Default" with no comment nor translations // which can get confused with the system's default theme if (theme->id == FALLBACK_THEME) { theme->name = i18nc("Name of the fallback \"freedesktop\" sound theme", "FreeDesktop"); theme->comment = i18n("Fallback sound theme from freedesktop.org"); } m_themes << theme; } } QCollator collator; // Sort by theme name, but leave "freedesktop" default at the last position std::sort(m_themes.begin(), m_themes.end(), [&collator](auto *a, auto *b) { if (a->id == FALLBACK_THEME) { return false; } if (b->id == FALLBACK_THEME) { return true; } return collator.compare(a->name, b->name) < 0; }); Q_EMIT themesLoaded(); Q_EMIT themeChanged(); } ca_context *KCMSoundTheme::canberraContext() { if (!m_canberraContext) { int ret = ca_context_create(&m_canberraContext); if (ret != CA_SUCCESS) { qCWarning(KCM_SOUNDTHEME) << "Failed to initialize canberra context for audio notification:" << ca_strerror(ret); m_canberraContext = nullptr; return nullptr; } // clang-format off ret = ca_context_change_props(m_canberraContext, CA_PROP_APPLICATION_NAME, qUtf8Printable(metaData().name()), CA_PROP_APPLICATION_ID, qUtf8Printable(metaData().pluginId()), CA_PROP_APPLICATION_ICON_NAME, qUtf8Printable(metaData().iconName()), nullptr); // clang-format on if (ret != CA_SUCCESS) { qCWarning(KCM_SOUNDTHEME) << "Failed to set application properties on canberra context for audio notification:" << ca_strerror(ret); } } return m_canberraContext; } int KCMSoundTheme::playSound(const QString &themeId, const QStringList &soundList) { ca_proplist *props = nullptr; ca_proplist_create(&props); ca_proplist_sets(props, CA_PROP_CANBERRA_XDG_THEME_NAME, themeId.toLatin1().constData()); ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "volatile"); // We don't want several previews playing at the same time ca_context_cancel(canberraContext(), 0); int result = CA_SUCCESS; for (const QString &soundName : soundList) { ca_proplist_sets(props, CA_PROP_EVENT_ID, soundName.toLatin1().constData()); result = ca_context_play_full(canberraContext(), 0, props, &ca_finish_callback, this); qCDebug(KCM_SOUNDTHEME) << "Try playing sound" << soundName << "for theme" << themeId << ":" << ca_strerror(result); if (result == CA_SUCCESS) { m_playingTheme = themeId; m_playingSound = soundName; Q_EMIT playingChanged(); break; } } ca_proplist_destroy(props); return result; } void KCMSoundTheme::cancelSound() { ca_context_cancel(canberraContext(), 0); } QString KCMSoundTheme::errorString(int errorCode) { return QString::fromUtf8(ca_strerror(errorCode)); } void KCMSoundTheme::ca_finish_callback(ca_context *c, uint32_t id, int error_code, void *userdata) { Q_UNUSED(c); Q_UNUSED(id); Q_UNUSED(error_code); QMetaObject::invokeMethod(static_cast(userdata), "onPlayingFinished"); } void KCMSoundTheme::onPlayingFinished() { m_playingTheme = QString(); m_playingSound = QString(); Q_EMIT playingChanged(); } ThemeInfo::ThemeInfo(const QString &themeId, QObject *parent) : QObject(parent) { const QStringList themeInfoSources = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/%1/index.theme").arg(themeId)); if (themeInfoSources.isEmpty()) { return; } KConfig config = KConfig(); config.addConfigSources(themeInfoSources); KConfigGroup themeGroup = config.group(u"Sound Theme"_s); if (!themeGroup.exists()) { return; } id = themeId; name = themeGroup.readEntry("Name", themeId); comment = themeGroup.readEntry("Comment", {}); inherits = themeGroup.readEntry("Inherits", QStringList()); directories = themeGroup.readEntry("Directories", QStringList()); isHidden = themeGroup.readEntry("Hidden", false); example = themeGroup.readEntry("Example", {}); isValid = true; } #include "kcm_soundtheme.moc"