diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,11 @@ include(KDEConnectMacros.cmake) -add_definitions(-DQT_NO_URL_CAST_FROM_STRING -DQT_NO_KEYWORDS) +include(FindPkgConfig) +pkg_check_modules(GCONF REQUIRED gconf-2.0) +pkg_check_modules(GOBJECT REQUIRED gobject-2.0) + +add_definitions(-DQT_NO_URL_CAST_FROM_STRING) include(GenerateExportHeader) include_directories(${CMAKE_CURRENT_BINARY_DIR}) diff --git a/cmake/FindCanberra.cmake b/cmake/FindCanberra.cmake new file mode 100644 --- /dev/null +++ b/cmake/FindCanberra.cmake @@ -0,0 +1,50 @@ +# - Find libcanberra's libraries and headers. +# This module defines the following variables: +# +# CANBERRA_FOUND - true if libcanberra was found +# CANBERRA_LIBRARIES - libcanberra libraries to link against +# CANBERRA_INCLUDE_DIRS - include path for libcanberra +# +# Copyright (c) 2012 Raphael Kubo da Costa +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the University nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +find_package(PkgConfig) +pkg_check_modules(PC_CANBERRA libcanberra) + +find_library(CANBERRA_LIBRARIES + NAMES canberra + HINTS ${PC_CANBERRA_LIBRARY_DIRS} ${PC_CANBERRA_LIBDIR} +) + +find_path(CANBERRA_INCLUDE_DIRS + NAMES canberra.h + HINTS ${PC_CANBERRA_INCLUDE_DIRS} ${PC_CANBERRA_INCLUDEDIR} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Canberra REQUIRED_VARS CANBERRA_LIBRARIES CANBERRA_INCLUDE_DIRS) + +mark_as_advanced(CANBERRA_LIBRARIES CANBERRA_INCLUDE_DIRS) diff --git a/cmake/FindGLIB2.cmake b/cmake/FindGLIB2.cmake new file mode 100644 --- /dev/null +++ b/cmake/FindGLIB2.cmake @@ -0,0 +1,52 @@ +# - Try to find the GLIB2 libraries +# Once done this will define +# +# GLIB2_FOUND - system has glib2 +# GLIB2_INCLUDE_DIR - the glib2 include directory +# GLIB2_LIBRARIES - glib2 library + +# Copyright (c) 2008 Laurent Montel, +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + + +if(GLIB2_INCLUDE_DIR AND GLIB2_LIBRARIES) + # Already in cache, be silent + set(GLIB2_FIND_QUIETLY TRUE) +endif(GLIB2_INCLUDE_DIR AND GLIB2_LIBRARIES) + +find_package(PkgConfig) +pkg_check_modules(PC_LibGLIB2 glib-2.0) + +find_path(GLIB2_MAIN_INCLUDE_DIR + NAMES glib.h + HINTS ${PC_LibGLIB2_INCLUDEDIR} + PATH_SUFFIXES glib-2.0) + +find_library(GLIB2_LIBRARY + NAMES glib-2.0 + HINTS ${PC_LibGLIB2_LIBDIR} +) + +set(GLIB2_LIBRARIES ${GLIB2_LIBRARY}) + +# search the glibconfig.h include dir under the same root where the library is found +get_filename_component(glib2LibDir "${GLIB2_LIBRARIES}" PATH) + +find_path(GLIB2_INTERNAL_INCLUDE_DIR glibconfig.h + PATH_SUFFIXES glib-2.0/include + HINTS ${PC_LibGLIB2_INCLUDEDIR} "${glib2LibDir}" ${CMAKE_SYSTEM_LIBRARY_PATH}) + +set(GLIB2_INCLUDE_DIR "${GLIB2_MAIN_INCLUDE_DIR}") + +# not sure if this include dir is optional or required +# for now it is optional +if(GLIB2_INTERNAL_INCLUDE_DIR) + set(GLIB2_INCLUDE_DIR ${GLIB2_INCLUDE_DIR} "${GLIB2_INTERNAL_INCLUDE_DIR}") +endif(GLIB2_INTERNAL_INCLUDE_DIR) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GLIB2 DEFAULT_MSG GLIB2_LIBRARIES GLIB2_MAIN_INCLUDE_DIR) + +mark_as_advanced(GLIB2_INCLUDE_DIR GLIB2_LIBRARIES) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -20,6 +20,7 @@ add_subdirectory(mousepad) add_subdirectory(screensaver-inhibit) add_subdirectory(sftp) + add_subdirectory(systemvolume) endif() if(EXPERIMENTALAPP_ENABLED) add_subdirectory(remotecommands) diff --git a/plugins/systemvolume/CMakeLists.txt b/plugins/systemvolume/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/CMakeLists.txt @@ -0,0 +1,47 @@ +set(kdeconnect_systemvolume_SRCS + systemvolumeplugin.cpp + card.cpp + client.cpp + context.cpp + device.cpp + maps.cpp + operation.cpp + port.cpp + profile.cpp + pulseaudio.cpp + pulseobject.cpp + sink.cpp + sinkinput.cpp + modulemanager.cpp + gconfitem.cpp + source.cpp + sourceoutput.cpp + stream.cpp + volumeobject.cpp + debug.cpp + server.cpp + streamrestore.cpp + module.cpp +) + +find_package(PulseAudio 5.0.0 REQUIRED) +find_package(Canberra REQUIRED) +find_package(GLIB2 REQUIRED) + +include_directories(${PULSEAUDIO_INCLUDE_DIR} ${GLIB2_INCLUDE_DIR} ${GCONF_INCLUDE_DIRS} + ${GOBJECT_INCLUDE_DIRS}) + + +kdeconnect_add_plugin(kdeconnect_systemvolume JSON kdeconnect_systemvolume.json SOURCES ${kdeconnect_systemvolume_SRCS}) + +target_link_libraries(kdeconnect_systemvolume + kdeconnectcore + Qt5::Core + Qt5::Gui + Qt5::DBus + ${PULSEAUDIO_LIBRARY} + ${PULSEAUDIO_MAINLOOP_LIBRARY} + ${GCONF_LDFLAGS} + ${GOBJECT_LDFLAGS} + ${CANBERRA_LIBRARIES} +) diff --git a/plugins/systemvolume/README b/plugins/systemvolume/README new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/README @@ -0,0 +1 @@ +This plugin allows to control the system volume. diff --git a/plugins/systemvolume/card.h b/plugins/systemvolume/card.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/card.h @@ -0,0 +1,111 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef CARD_H +#define CARD_H + +#include + +#include +#include + +#include "profile.h" +#include "port.h" +#include "pulseobject.h" + +namespace QPulseAudio +{ + +class CardPort : public Port +{ + Q_OBJECT + Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged) +public: + CardPort(QObject *parent = nullptr) : Port(parent) {} + virtual ~CardPort() {} + +// int direction; /**< A #pa_direction enum, indicating the direction of this port. */ +// uint32_t n_profiles; /**< Number of entries in profile array */ +// pa_card_profile_info** profiles; /**< \deprecated Superseded by profiles2 */ +// int64_t latency_offset; /**< Latency offset of the port that gets added to the sink/source latency when the port is active. \since 3.0 */ +// pa_card_profile_info2** profiles2; /**< Array of pointers to available profiles, or NULL. Array is terminated by an entry set to NULL. \since 5.0 */ + + void update(const pa_card_port_info *info) + { + setInfo(info); + + m_properties.clear(); + void *it = nullptr; + while (const char *key = pa_proplist_iterate(info->proplist, &it)) { + Q_ASSERT(key); + const char *value = pa_proplist_gets(info->proplist, key); + if (!value) { + qCDebug(PLASMAPA) << "property" << key << "not a string"; + continue; + } + Q_ASSERT(value); + m_properties.insert(QString::fromUtf8(key), QString::fromUtf8(value)); + } + emit propertiesChanged(); + } + + QVariantMap properties() const { return m_properties; } + +signals: + void propertiesChanged(); + +private: + QVariantMap m_properties; +}; + +class Card : public PulseObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QList profiles READ profiles NOTIFY profilesChanged) + Q_PROPERTY(quint32 activeProfileIndex READ activeProfileIndex WRITE setActiveProfileIndex NOTIFY activeProfileIndexChanged) + Q_PROPERTY(QList ports READ ports NOTIFY portsChanged) +public: + Card(QObject *parent); + + void update(const pa_card_info *info); + + QString name() const; + QList profiles() const; + quint32 activeProfileIndex() const; + void setActiveProfileIndex(quint32 profileIndex); + QList ports() const; + +signals: + void nameChanged(); + void profilesChanged(); + void activeProfileIndexChanged(); + void portsChanged(); + +private: + QString m_name; + QList m_profiles; + quint32 m_activeProfileIndex; + QList m_ports; +}; + +} // QPulseAudio + +#endif // CARD_H diff --git a/plugins/systemvolume/card.cpp b/plugins/systemvolume/card.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/card.cpp @@ -0,0 +1,94 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "card.h" + +#include "debug.h" + +#include "context.h" + +namespace QPulseAudio +{ + +Card::Card(QObject *parent) + : PulseObject(parent) +{ +} + +void Card::update(const pa_card_info *info) +{ + updatePulseObject(info); + + QString infoName = QString::fromUtf8(info->name); + if (m_name != infoName) { + m_name = infoName; + emit nameChanged(); + } + + qDeleteAll(m_profiles); + m_profiles.clear(); + for (auto **it = info->profiles2; it && *it != nullptr; ++it) { + Profile *profile = new Profile(this); + profile->setInfo(*it); + m_profiles.append(profile); + if (info->active_profile2 == *it) { + m_activeProfileIndex = m_profiles.length() - 1; + } + } + emit profilesChanged(); + emit activeProfileIndexChanged(); + + qDeleteAll(m_ports); + m_ports.clear(); + for (auto **it = info->ports; it && *it != nullptr; ++it) { + CardPort *port = new CardPort(this); + port->update(*it); + m_ports.append(port); + } + emit portsChanged(); +} + +QString Card::name() const +{ + return m_name; +} + +QList Card::profiles() const +{ + return m_profiles; +} + +quint32 Card::activeProfileIndex() const +{ + return m_activeProfileIndex; +} + +void Card::setActiveProfileIndex(quint32 profileIndex) +{ + const Profile *profile = qobject_cast(profiles().at(profileIndex)); + context()->setCardProfile(index(), profile->name()); +} + +QList Card::ports() const +{ + return m_ports; +} + +} // QPulseAudio diff --git a/plugins/systemvolume/client.h b/plugins/systemvolume/client.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/client.h @@ -0,0 +1,54 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef CLIENT_H +#define CLIENT_H + +#include + +#include + +#include "pulseobject.h" + +namespace QPulseAudio +{ + +class Client : public PulseObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name NOTIFY nameChanged) +public: + Client(QObject *parent); + virtual ~Client(); + + void update(const pa_client_info *info); + + QString name() const; + +signals: + void nameChanged(); + +private: + QString m_name; +}; + +} // QPulseAudio + +#endif // CLIENT_H diff --git a/plugins/systemvolume/client.cpp b/plugins/systemvolume/client.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/client.cpp @@ -0,0 +1,53 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "client.h" + +#include "debug.h" + +namespace QPulseAudio +{ + +Client::Client(QObject *parent) + : PulseObject(parent) +{ +} + +Client::~Client() +{ +} + +void Client::update(const pa_client_info *info) +{ + updatePulseObject(info); + + QString infoName = QString::fromUtf8(info->name); + if (m_name != infoName) { + m_name = infoName; + emit nameChanged(); + } +} + +QString Client::name() const +{ + return m_name; +} + +} // QPulseAudio diff --git a/plugins/systemvolume/context.h b/plugins/systemvolume/context.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/context.h @@ -0,0 +1,186 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef CONTEXT_H +#define CONTEXT_H + +#include +#include +#include + +#include +#include +#include +#include + +#include "maps.h" +#include "operation.h" + +namespace QPulseAudio +{ + +class Server; + +class Context : public QObject +{ + Q_OBJECT +public: + Context(QObject *parent = nullptr); + ~Context(); + + static Context *instance(); + + static const qint64 NormalVolume = PA_VOLUME_NORM; + static const qint64 MinimalVolume = 0; + static const qint64 MaximalVolume = (PA_VOLUME_NORM / 100.0) * 150; + + void ref(); + void unref(); + + bool isValid() { return m_context && m_mainloop; } + + const SinkMap &sinks() const { return m_sinks; } + const SinkInputMap &sinkInputs() const { return m_sinkInputs; } + const SourceMap &sources() const { return m_sources; } + const SourceOutputMap &sourceOutputs() const { return m_sourceOutputs; } + const ClientMap &clients() const { return m_clients; } + const CardMap &cards() const { return m_cards; } + const ModuleMap &modules() const { return m_modules; } + const StreamRestoreMap &streamRestores() const { return m_streamRestores; } + Server *server() const { return m_server; } + QString newDefaultSink() const { return m_newDefaultSink; } + QString newDefaultSource() const { return m_newDefaultSource; } + + void subscribeCallback(pa_context *context, pa_subscription_event_type_t type, uint32_t index); + void contextStateCallback(pa_context *context); + + void sinkCallback(const pa_sink_info *info); + void sinkInputCallback(const pa_sink_input_info *info); + void sourceCallback(const pa_source_info *info); + void sourceOutputCallback(const pa_source_output_info *info); + void clientCallback(const pa_client_info *info); + void cardCallback(const pa_card_info *info); + void moduleCallback(const pa_module_info *info); + void streamRestoreCallback(const pa_ext_stream_restore_info *info); + void serverCallback(const pa_server_info *info); + + void setCardProfile(quint32 index, const QString &profile); + void setDefaultSink(const QString &name); + void setDefaultSource(const QString &name); + void streamRestoreWrite(const pa_ext_stream_restore_info *info); + + template + void setGenericVolume(quint32 index, int channel, qint64 newVolume, + pa_cvolume cVolume, PAFunction pa_set_volume) + { + if (!m_context) { + return; + } + newVolume = qBound(0, newVolume, PA_VOLUME_MAX); + pa_cvolume newCVolume = cVolume; + if (channel == -1) { // -1 all channels + const qint64 diff = newVolume - pa_cvolume_avg(&cVolume); + for (int i = 0; i < newCVolume.channels; ++i) { + newCVolume.values[i] = qBound(0, newCVolume.values[i] + diff, PA_VOLUME_MAX); + } + } else { + Q_ASSERT(newCVolume.channels > channel); + newCVolume.values[channel] = newVolume; + } + if (!PAOperation(pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr))) { + qCWarning(PLASMAPA) << "pa_set_volume failed"; + return; + } + } + + template + void setGenericMute(quint32 index, bool mute, PAFunction pa_set_mute) + { + if (!m_context) { + return; + } + if (!PAOperation(pa_set_mute(m_context, index, mute, nullptr, nullptr))) { + qCWarning(PLASMAPA) << "pa_set_mute failed"; + return; + } + } + + template + void setGenericPort(quint32 index, const QString &portName, PAFunction pa_set_port) + { + if (!m_context) { + return; + } + if (!PAOperation(pa_set_port(m_context, + index, + portName.toUtf8().constData(), + nullptr, + nullptr))) { + qCWarning(PLASMAPA) << "pa_set_port failed"; + return; + } + } + + template + void setGenericDeviceForStream(quint32 streamIndex, + quint32 deviceIndex, + PAFunction pa_move_stream_to_device) + { + if (!m_context) { + return; + } + if (!PAOperation(pa_move_stream_to_device(m_context, + streamIndex, + deviceIndex, + nullptr, + nullptr))) { + qCWarning(PLASMAPA) << "pa_move_stream_to_device failed"; + return; + } + } + +private: + void connectToDaemon(); + void reset(); + + // Don't forget to add things to reset(). + SinkMap m_sinks; + SinkInputMap m_sinkInputs; + SourceMap m_sources; + SourceOutputMap m_sourceOutputs; + ClientMap m_clients; + CardMap m_cards; + ModuleMap m_modules; + StreamRestoreMap m_streamRestores; + Server *m_server; + + pa_context *m_context; + pa_glib_mainloop *m_mainloop; + + QString m_newDefaultSink; + QString m_newDefaultSource; + + int m_references; + static Context* s_context; +}; + +} // QPulseAudio + +#endif // CONTEXT_H diff --git a/plugins/systemvolume/context.cpp b/plugins/systemvolume/context.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/context.cpp @@ -0,0 +1,633 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "context.h" +#include "server.h" + +#include +#include "debug.h" +#include +#include +#include +#include + +#include "card.h" +#include "client.h" +#include "sink.h" +#include "sinkinput.h" +#include "source.h" +#include "sourceoutput.h" +#include "streamrestore.h" +#include "module.h" + +namespace QPulseAudio +{ + +Context* Context::s_context = nullptr; + +static bool isGoodState(int eol) +{ + if (eol < 0) { + // Error + return false; + } + + if (eol > 0) { + // End of callback chain + return false; + } + + return true; +} + +// -------------------------- + +static void sink_cb(pa_context *context, const pa_sink_info *info, int eol, void *data) +{ + if (!isGoodState(eol)) + return; + Q_ASSERT(context); + Q_ASSERT(data); + ((Context *)data)->sinkCallback(info); +} + +static void sink_input_callback(pa_context *context, const pa_sink_input_info *info, int eol, void *data) +{ + if (!isGoodState(eol)) + return; + // pulsesink probe is used by gst-pulse only to query sink formats (not for playback) + if (qstrcmp(info->name, "pulsesink probe") == 0) { + return; + } + if (const char *id = pa_proplist_gets(info->proplist, "module-stream-restore.id")) { + if (qstrcmp(id, "sink-input-by-media-role:event") == 0) { + qCDebug(PLASMAPA) << "Ignoring event role sink input."; + return; + } + } + Q_ASSERT(context); + Q_ASSERT(data); + ((Context *)data)->sinkInputCallback(info); +} + +static void source_cb(pa_context *context, const pa_source_info *info, int eol, void *data) +{ + if (!isGoodState(eol)) + return; + // FIXME: This forces excluding monitors + if (info->monitor_of_sink != PA_INVALID_INDEX) + return; + Q_ASSERT(context); + Q_ASSERT(data); + ((Context *)data)->sourceCallback(info); +} + +static void source_output_cb(pa_context *context, const pa_source_output_info *info, int eol, void *data) +{ + if (!isGoodState(eol)) + return; + // FIXME: This forces excluding these apps + if (const char *app = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_ID)) { + if (strcmp(app, "org.PulseAudio.pavucontrol") == 0 + || strcmp(app, "org.gnome.VolumeControl") == 0 + || strcmp(app, "org.kde.kmixd") == 0) + return; + } + Q_ASSERT(context); + Q_ASSERT(data); + ((Context *)data)->sourceOutputCallback(info); +} + +static void client_cb(pa_context *context, const pa_client_info *info, int eol, void *data) +{ + if (!isGoodState(eol)) + return; + Q_ASSERT(context); + Q_ASSERT(data); + ((Context *)data)->clientCallback(info); +} + +static void card_cb(pa_context *context, const pa_card_info *info, int eol, void *data) +{ + if (!isGoodState(eol)) + return; + Q_ASSERT(context); + Q_ASSERT(data); + ((Context *)data)->cardCallback(info); +} + +static void module_info_list_cb(pa_context *context, const pa_module_info *info, int eol, void *data) +{ + if (!isGoodState(eol)) + return; + Q_ASSERT(context); + Q_ASSERT(data); + ((Context *)data)->moduleCallback(info); +} + +static void server_cb(pa_context *context, const pa_server_info *info, void *data) +{ + Q_ASSERT(context); + Q_ASSERT(data); + ((Context *)data)->serverCallback(info); +} + +static void context_state_callback(pa_context *context, void *data) +{ + Q_ASSERT(data); + ((Context *)data)->contextStateCallback(context); +} + +static void subscribe_cb(pa_context *context, pa_subscription_event_type_t type, uint32_t index, void *data) +{ + Q_ASSERT(data); + ((Context *)data)->subscribeCallback(context, type, index); +} + +static void ext_stream_restore_read_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data) +{ + if (!isGoodState(eol)) { + return; + } + Q_ASSERT(context); + Q_ASSERT(data); + ((Context *)data)->streamRestoreCallback(info); +} + +static void ext_stream_restore_subscribe_cb(pa_context *context, void *data) +{ + Q_ASSERT(context); + Q_ASSERT(data); + if (!PAOperation(pa_ext_stream_restore_read(context, ext_stream_restore_read_cb, data))) { + qCWarning(PLASMAPA) << "pa_ext_stream_restore_read() failed"; + } +} + +static void ext_stream_restore_change_sink_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data) +{ + if (!isGoodState(eol)) { + return; + } + Q_ASSERT(context); + Q_ASSERT(data); + if (qstrncmp(info->name, "sink-input-by", 13) == 0) { + Context *context = static_cast(data); + const QByteArray deviceData = context->newDefaultSink().toUtf8(); + pa_ext_stream_restore_info newinfo; + newinfo.name = info->name; + newinfo.channel_map = info->channel_map; + newinfo.volume = info->volume; + newinfo.mute = info->mute; + newinfo.device = deviceData.constData(); + context->streamRestoreWrite(&newinfo); + } +} + +static void ext_stream_restore_change_source_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data) +{ + if (!isGoodState(eol)) { + return; + } + Q_ASSERT(context); + Q_ASSERT(data); + if (qstrncmp(info->name, "source-output-by", 16) == 0) { + Context *context = static_cast(data); + const QByteArray deviceData = context->newDefaultSource().toUtf8(); + pa_ext_stream_restore_info newinfo; + newinfo.name = info->name; + newinfo.channel_map = info->channel_map; + newinfo.volume = info->volume; + newinfo.mute = info->mute; + newinfo.device = deviceData.constData(); + context->streamRestoreWrite(&newinfo); + } +} + +// -------------------------- + +Context::Context(QObject *parent) + : QObject(parent) + , m_server(new Server(this)) + , m_context(nullptr) + , m_mainloop(nullptr) + , m_references(0) +{ + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(QStringLiteral("org.pulseaudio.Server"), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForRegistration, + this); + connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &Context::connectToDaemon); + connectToDaemon(); +} + +Context::~Context() +{ + if (m_context) { + pa_context_unref(m_context); + m_context = nullptr; + } + + if (m_mainloop) { + pa_glib_mainloop_free(m_mainloop); + m_mainloop = nullptr; + } + + reset(); +} + +Context *Context::instance() +{ + if (!s_context) { + s_context = new Context; + } + return s_context; +} + +void Context::ref() +{ + ++m_references; +} + +void Context::unref() +{ + if (--m_references == 0) { + delete this; + s_context = nullptr; + } +} + +void Context::subscribeCallback(pa_context *context, pa_subscription_event_type_t type, uint32_t index) +{ + Q_ASSERT(context == m_context); + + switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { + case PA_SUBSCRIPTION_EVENT_SINK: + if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + m_sinks.removeEntry(index); + } else { + if (!PAOperation(pa_context_get_sink_info_by_index(context, index, sink_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_sink_info_by_index() failed"; + return; + } + } + break; + + case PA_SUBSCRIPTION_EVENT_SOURCE: + if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + m_sources.removeEntry(index); + } else { + if (!PAOperation(pa_context_get_source_info_by_index(context, index, source_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_source_info_by_index() failed"; + return; + } + } + break; + + case PA_SUBSCRIPTION_EVENT_SINK_INPUT: + if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + m_sinkInputs.removeEntry(index); + } else { + if (!PAOperation(pa_context_get_sink_input_info(context, index, sink_input_callback, this))) { + qCWarning(PLASMAPA) << "pa_context_get_sink_input_info() failed"; + return; + } + } + break; + + case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: + if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + m_sourceOutputs.removeEntry(index); + } else { + if (!PAOperation(pa_context_get_source_output_info(context, index, source_output_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_sink_input_info() failed"; + return; + } + } + break; + + case PA_SUBSCRIPTION_EVENT_CLIENT: + if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + m_clients.removeEntry(index); + } else { + if (!PAOperation(pa_context_get_client_info(context, index, client_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_client_info() failed"; + return; + } + } + break; + + case PA_SUBSCRIPTION_EVENT_CARD: + if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + m_cards.removeEntry(index); + } else { + if (!PAOperation(pa_context_get_card_info_by_index(context, index, card_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_card_info_by_index() failed"; + return; + } + } + break; + + case PA_SUBSCRIPTION_EVENT_MODULE: + if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + m_modules.removeEntry(index); + } else { + if (!PAOperation(pa_context_get_module_info_list(context, module_info_list_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_module_info_list() failed"; + return; + } + } + break; + + + case PA_SUBSCRIPTION_EVENT_SERVER: + if (!PAOperation(pa_context_get_server_info(context, server_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_server_info() failed"; + return; + } + break; + + } +} + +void Context::contextStateCallback(pa_context *c) +{ + qCDebug(PLASMAPA) << "state callback"; + pa_context_state_t state = pa_context_get_state(c); + if (state == PA_CONTEXT_READY) { + qCDebug(PLASMAPA) << "ready"; + + // 1. Register for the stream changes (except during probe) + if (m_context == c) { + pa_context_set_subscribe_callback(c, subscribe_cb, this); + + if (!PAOperation(pa_context_subscribe(c, (pa_subscription_mask_t) + (PA_SUBSCRIPTION_MASK_SINK| + PA_SUBSCRIPTION_MASK_SOURCE| + PA_SUBSCRIPTION_MASK_CLIENT| + PA_SUBSCRIPTION_MASK_SINK_INPUT| + PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT| + PA_SUBSCRIPTION_MASK_CARD| + PA_SUBSCRIPTION_MASK_MODULE| + PA_SUBSCRIPTION_MASK_SERVER), nullptr, nullptr))) { + qCWarning(PLASMAPA) << "pa_context_subscribe() failed"; + return; + } + } + + if (!PAOperation(pa_context_get_sink_info_list(c, sink_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_sink_info_list() failed"; + return; + } + + if (!PAOperation(pa_context_get_source_info_list(c, source_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_source_info_list() failed"; + return; + } + + if (!PAOperation(pa_context_get_client_info_list(c, client_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_client_info_list() failed"; + return; + } + + if (!PAOperation(pa_context_get_card_info_list(c, card_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_card_info_list() failed"; + return; + } + + if (!PAOperation(pa_context_get_sink_input_info_list(c, sink_input_callback, this))) { + qCWarning(PLASMAPA) << "pa_context_get_sink_input_info_list() failed"; + return; + } + + if (!PAOperation(pa_context_get_source_output_info_list(c, source_output_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_source_output_info_list() failed"; + return; + } + + if (!PAOperation(pa_context_get_module_info_list(c, module_info_list_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_module_info_list() failed"; + return; + } + + if (!PAOperation(pa_context_get_server_info(c, server_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_server_info() failed"; + return; + } + + if (PAOperation(pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, this))) { + pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb, this); + PAOperation(pa_ext_stream_restore_subscribe(c, 1, nullptr, this)); + } else { + qCWarning(PLASMAPA) << "Failed to initialize stream_restore extension"; + } + } else if (!PA_CONTEXT_IS_GOOD(state)) { + qCWarning(PLASMAPA) << "context kaput"; + if (m_context) { + pa_context_unref(m_context); + m_context = nullptr; + } + reset(); + QTimer::singleShot(1000, this, &Context::connectToDaemon); + } +} + +void Context::sinkCallback(const pa_sink_info *info) +{ + // This parenting here is a bit weird + m_sinks.updateEntry(info, this); +} + +void Context::sinkInputCallback(const pa_sink_input_info *info) +{ + m_sinkInputs.updateEntry(info, this); +} + +void Context::sourceCallback(const pa_source_info *info) +{ + m_sources.updateEntry(info, this); +} + +void Context::sourceOutputCallback(const pa_source_output_info *info) +{ + m_sourceOutputs.updateEntry(info, this); +} + +void Context::clientCallback(const pa_client_info *info) +{ + m_clients.updateEntry(info, this); +} + +void Context::cardCallback(const pa_card_info *info) +{ + m_cards.updateEntry(info, this); +} + +void Context::moduleCallback(const pa_module_info *info) +{ + m_modules.updateEntry(info, this); +} + +void Context::streamRestoreCallback(const pa_ext_stream_restore_info *info) +{ + if (qstrcmp(info->name, "sink-input-by-media-role:event") != 0) { + return; + } + + const int eventRoleIndex = 1; + StreamRestore *obj = qobject_cast(m_streamRestores.data().value(eventRoleIndex)); + + if (!obj) { + QVariantMap props; + props.insert(QStringLiteral("application.icon_name"), + QStringLiteral("preferences-desktop-notification")); + obj = new StreamRestore(eventRoleIndex, props, this); + m_streamRestores.insert(obj); + } + + obj->update(info); +} + +void Context::serverCallback(const pa_server_info *info) +{ + m_server->update(info); +} + +void Context::setCardProfile(quint32 index, const QString &profile) +{ + if (!m_context) { + return; + } + qCDebug(PLASMAPA) << index << profile; + if (!PAOperation(pa_context_set_card_profile_by_index(m_context, + index, + profile.toUtf8().constData(), + nullptr, nullptr))) { + qCWarning(PLASMAPA) << "pa_context_set_card_profile_by_index failed"; + return; + } +} + +void Context::setDefaultSink(const QString &name) +{ + if (!m_context) { + return; + } + const QByteArray nameData = name.toUtf8(); + if (!PAOperation(pa_context_set_default_sink(m_context, + nameData.constData(), + nullptr, + nullptr))) { + qCWarning(PLASMAPA) << "pa_context_set_default_sink failed"; + } + + // Change device for all entries in stream-restore database + m_newDefaultSink = name; + if (!PAOperation(pa_ext_stream_restore_read(m_context, + ext_stream_restore_change_sink_cb, + this))) { + qCWarning(PLASMAPA) << "pa_ext_stream_restore_read failed"; + } +} + +void Context::setDefaultSource(const QString &name) +{ + if (!m_context) { + return; + } + const QByteArray nameData = name.toUtf8(); + if (!PAOperation(pa_context_set_default_source(m_context, + nameData.constData(), + nullptr, + nullptr))) { + qCWarning(PLASMAPA) << "pa_context_set_default_source failed"; + } + + // Change device for all entries in stream-restore database + m_newDefaultSource = name; + if (!PAOperation(pa_ext_stream_restore_read(m_context, + ext_stream_restore_change_source_cb, + this))) { + qCWarning(PLASMAPA) << "pa_ext_stream_restore_read failed"; + } +} + +void Context::streamRestoreWrite(const pa_ext_stream_restore_info *info) +{ + if (!m_context) { + return; + } + if (!PAOperation(pa_ext_stream_restore_write(m_context, + PA_UPDATE_REPLACE, + info, + 1, + true, + nullptr, + nullptr))) { + qCWarning(PLASMAPA) << "pa_ext_stream_restore_write failed"; + } +} + +void Context::connectToDaemon() +{ + if (m_context) { + return; + } + + // We require a glib event loop + if (!QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("EventDispatcherGlib")) { + qCWarning(PLASMAPA) << "Disabling PulseAudio integration for lack of GLib event loop"; + return; + } + + qCDebug(PLASMAPA) << "Attempting connection to PulseAudio sound daemon"; + if (!m_mainloop) { + m_mainloop = pa_glib_mainloop_new(nullptr); + Q_ASSERT(m_mainloop); + } + + pa_mainloop_api *api = pa_glib_mainloop_get_api(m_mainloop); + Q_ASSERT(api); + m_context = pa_context_new(api, "QPulse"); + Q_ASSERT(m_context); + + if (pa_context_connect(m_context, NULL, PA_CONTEXT_NOFAIL, nullptr) < 0) { + pa_context_unref(m_context); + pa_glib_mainloop_free(m_mainloop); + m_context = nullptr; + m_mainloop = nullptr; + return; + } + pa_context_set_state_callback(m_context, &context_state_callback, this); +} + +void Context::reset() +{ + m_sinks.reset(); + m_sinkInputs.reset(); + m_sources.reset(); + m_sourceOutputs.reset(); + m_clients.reset(); + m_cards.reset(); + m_modules.reset(); + m_streamRestores.reset(); + m_server->reset(); +} + +} // QPulseAudio diff --git a/plugins/systemvolume/debug.h b/plugins/systemvolume/debug.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/debug.h @@ -0,0 +1,27 @@ +/* This file is part of the KDE project + Copyright (C) 2015 Bhushan Shah + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef DEBUG_H +#define DEBUG_H + +#include +Q_DECLARE_LOGGING_CATEGORY(PLASMAPA) + +#endif + diff --git a/plugins/systemvolume/debug.cpp b/plugins/systemvolume/debug.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/debug.cpp @@ -0,0 +1,22 @@ +/* This file is part of the KDE project + Copyright (C) 2015 Bhushan Shah + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "debug.h" + +Q_LOGGING_CATEGORY(PLASMAPA, "org.kde.plasma.pulseaudio", QtWarningMsg) diff --git a/plugins/systemvolume/device.h b/plugins/systemvolume/device.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/device.h @@ -0,0 +1,145 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef PULSE_DEVICE_H +#define PULSE_DEVICE_H + +#include + +#include + +#include "volumeobject.h" +#include "port.h" +#include "pulseobject.h" + +namespace QPulseAudio +{ + +class Device : public VolumeObject +{ + Q_OBJECT + Q_PROPERTY(State state READ state NOTIFY stateChanged) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) + Q_PROPERTY(QString formFactor READ formFactor NOTIFY formFactorChanged) + Q_PROPERTY(quint32 cardIndex READ cardIndex NOTIFY cardIndexChanged) + Q_PROPERTY(QList ports READ ports NOTIFY portsChanged) + Q_PROPERTY(quint32 activePortIndex READ activePortIndex WRITE setActivePortIndex NOTIFY activePortIndexChanged) + Q_PROPERTY(bool default READ isDefault WRITE setDefault NOTIFY defaultChanged) +public: + enum State { + InvalidState = 0, + RunningState, + IdleState, + SuspendedState, + UnknownState + }; + Q_ENUMS(State); + + virtual ~Device() {} + + template + void updateDevice(const PAInfo *info) + { + updateVolumeObject(info); + + if (m_name != info->name) { + m_name = info->name; + emit nameChanged(); + } + if (m_description != info->description) { + m_description = info->description; + emit descriptionChanged(); + } + const char *form_factor = pa_proplist_gets(info->proplist, PA_PROP_DEVICE_FORM_FACTOR); + if (form_factor) { + QString formFactor = QString::fromUtf8(form_factor); + if (m_formFactor != formFactor) { + m_formFactor = formFactor; + emit formFactorChanged(); + } + } + + m_cardIndex = info->card; + emit cardIndexChanged(); + + // TODO: this rebuilds the entire port list on every update. would be + // nicer if it actually removed what needs removing updates what needs + // updating and adds what needs adding. Alas, this is a tad more + // involved. + qDeleteAll(m_ports); + m_ports.clear(); + for (auto **ports = info->ports; ports && *ports != nullptr; ++ports) { + Port *port = new Port(this); + port->setInfo(*ports); + m_ports.append(port); + if (info->active_port == *ports) { + m_activePortIndex = m_ports.length() - 1; + } + } + emit portsChanged(); + emit activePortIndexChanged(); + + State infoState = stateFromPaState(info->state); + if (infoState != m_state) { + m_state = infoState; + emit stateChanged(); + } + } + + State state() const; + QString name() const; + QString description() const; + QString formFactor() const; + quint32 cardIndex() const; + QList ports() const; + quint32 activePortIndex() const; + virtual void setActivePortIndex(quint32 port_index) = 0; + virtual bool isDefault() const = 0; + virtual void setDefault(bool enable) = 0; + +signals: + void stateChanged(); + void nameChanged(); + void descriptionChanged(); + void formFactorChanged(); + void cardIndexChanged(); + void portsChanged(); + void activePortIndexChanged(); + void defaultChanged(); + +protected: + Device(QObject *parent); + +private: + State stateFromPaState(int value) const; + + QString m_name; + QString m_description; + QString m_formFactor; + quint32 m_cardIndex = -1; + QList m_ports; + quint32 m_activePortIndex = -1; + State m_state = UnknownState; +}; + +} // QPulseAudio + +#endif // PULSE_DEVICE_H diff --git a/plugins/systemvolume/device.cpp b/plugins/systemvolume/device.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/device.cpp @@ -0,0 +1,77 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "device.h" + +QPulseAudio::Device::State QPulseAudio::Device::state() const +{ + return m_state; +} + +QString QPulseAudio::Device::name() const +{ + return m_name; +} + +QString QPulseAudio::Device::description() const +{ + return m_description; +} + +QString QPulseAudio::Device::formFactor() const +{ + return m_formFactor; +} + +quint32 QPulseAudio::Device::cardIndex() const +{ + return m_cardIndex; +} + +QList QPulseAudio::Device::ports() const +{ + return m_ports; +} + +quint32 QPulseAudio::Device::activePortIndex() const +{ + return m_activePortIndex; +} + +QPulseAudio::Device::Device(QObject *parent) + : VolumeObject(parent) +{ +} + +QPulseAudio::Device::State QPulseAudio::Device::stateFromPaState(int value) const +{ + switch (value) { + case -1: // PA_X_INVALID_STATE + return InvalidState; + case 0: // PA_X_RUNNING + return RunningState; + case 1: // PA_X_IDLE + return IdleState; + case 2: // PA_X_SUSPENDED + return SuspendedState; + default: + return UnknownState; + } +} diff --git a/plugins/systemvolume/gconfitem.h b/plugins/systemvolume/gconfitem.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/gconfitem.h @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2009 Nokia Corporation. + * Copyright (C) 2016 David Edmundson + * + * Contact: Marius Vollmer + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + */ + +#ifndef GCONFITEM_H +#define GCONFITEM_H + +#include +#include +#include + +/*! + + \brief GConfItem is a simple C++ wrapper for GConf. + + Creating a GConfItem instance gives you access to a single GConf + key. You can get and set its value, and connect to its + valueChanged() signal to be notified about changes. + + The value of a GConf key is returned to you as a QVariant, and you + pass in a QVariant when setting the value. GConfItem converts + between a QVariant and GConf values as needed, and according to the + following rules: + + - A QVariant of type QVariant::Invalid denotes an unset GConf key. + + - QVariant::Int, QVariant::Double, QVariant::Bool are converted to + and from the obvious equivalents. + + - QVariant::String is converted to/from a GConf string and always + uses the UTF-8 encoding. No other encoding is supported. + + - QVariant::StringList is converted to a list of UTF-8 strings. + + - QVariant::List (which denotes a QList) is converted + to/from a GConf list. All elements of such a list must have the + same type, and that type must be one of QVariant::Int, + QVariant::Double, QVariant::Bool, or QVariant::String. (A list of + strings is returned as a QVariant::StringList, however, when you + get it back.) + + - Any other QVariant or GConf value is essentially ignored. + + - This is fored by Dave from libqtgconf to really reduce the amount of QObjects needed + to manipulate various items in a tree. + + + \warning GConfItem is as thread-safe as GConf. + +*/ + + +class GConfItem : public QObject +{ + Q_OBJECT + + public: + /*! Initializes a GConfItem to access the GConf key denoted by + \a key. Key names should follow the normal GConf conventions + like "/myapp/settings/first". + + \param key The name of the key. + \param parent Parent object + */ + explicit GConfItem(const QString &keyRoot, QObject *parent = 0); + + /*! Finalizes a GConfItem. + */ + virtual ~GConfItem(); + + /*! Returns the root of this item, as given to the constructor. + */ + QString root() const; + + /*! Returns the current value of this item, as a QVariant. + * subkey is realtive to the provided root. + */ + QVariant value(const QString &subKey) const; + + /*! Returns the current value of this item, as a QVariant. If + * there is no value for this item, return \a def instead. + + */ + void set(const QString &subKey, const QVariant &val); + + /*! Return a list of the directories below this item. The + returned strings are absolute key names like + "/myapp/settings". + + A directory is a key that has children. The same key might + also have a value, but that is confusing and best avoided. + */ + QList listDirs() const; + + /*! Return a list of entries below this item. The returned + strings are absolute key names like "/myapp/settings/first". + + A entry is a key that has a value. The same key might also + have children, but that is confusing and is best avoided. + */ + QList listEntries() const; + + Q_SIGNALS: + /*! Emitted when some value in subtree of this item changes + */ + + void subtreeChanged(const QString& key, const QVariant& value); + + private: + friend struct GConfItemPrivate; + struct GConfItemPrivate *priv; + + void update_value(bool emit_signal, const QString& key, const QVariant& value); +}; + +#endif // GCONFITEM_H diff --git a/plugins/systemvolume/gconfitem.cpp b/plugins/systemvolume/gconfitem.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/gconfitem.cpp @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). + * All rights reserved. + * Copyright (C) 2016 David Edmundson + * + * Contact: Marius Vollmer + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + */ + +#include +#include +#include +#include +#include + +#include "gconfitem.h" + +#include +#include +#include + +struct GConfItemPrivate +{ + QString root; + QVariant value; + guint notify_id; + + static void notify_trampoline(GConfClient*, guint, GConfEntry *, gpointer); +}; + +#define withClient(c) for(GConfClient *c = (gconf_client_get_default()); c; g_object_unref(c), c=NULL) + +static QByteArray convertKey(QString key) +{ + if (key.startsWith('/')) { + return key.toUtf8(); + } else { + qWarning() << "Using dot-separated key names with GConfItem is deprecated."; + qWarning() << "Please use" << '/' + key.replace('.', '/') << "instead of" << key; + return '/' + key.replace('.', '/').toUtf8(); + } +} + +static QString convertKey(const char *key) +{ + return QString::fromUtf8(key); +} + +static QVariant convertValue(GConfValue *src) +{ + if (!src) { + return QVariant(); + } else { + switch (src->type) { + case GCONF_VALUE_INVALID: + return QVariant(QVariant::Invalid); + case GCONF_VALUE_BOOL: + return QVariant((bool)gconf_value_get_bool(src)); + case GCONF_VALUE_INT: + return QVariant(gconf_value_get_int(src)); + case GCONF_VALUE_FLOAT: + return QVariant(gconf_value_get_float(src)); + case GCONF_VALUE_STRING: + return QVariant(QString::fromUtf8(gconf_value_get_string(src))); + case GCONF_VALUE_LIST: + switch (gconf_value_get_list_type(src)) { + case GCONF_VALUE_STRING: + { + QStringList result; + for (GSList *elts = gconf_value_get_list(src); elts; elts = elts->next) + result.append(QString::fromUtf8(gconf_value_get_string((GConfValue *)elts->data))); + return QVariant(result); + } + default: + { + QList result; + for (GSList *elts = gconf_value_get_list(src); elts; elts = elts->next) + result.append(convertValue((GConfValue *)elts->data)); + return QVariant(result); + } + } + case GCONF_VALUE_SCHEMA: + default: + return QVariant(); + } + } +} + +static GConfValue *convertString(const QString &str) +{ + GConfValue *v = gconf_value_new (GCONF_VALUE_STRING); + gconf_value_set_string (v, str.toUtf8().data()); + return v; +} + +static GConfValueType primitiveType (const QVariant &elt) +{ + switch(elt.type()) { + case QVariant::String: + return GCONF_VALUE_STRING; + case QVariant::Int: + return GCONF_VALUE_INT; + case QVariant::Double: + return GCONF_VALUE_FLOAT; + case QVariant::Bool: + return GCONF_VALUE_BOOL; + default: + return GCONF_VALUE_INVALID; + } +} + +static GConfValueType uniformType(const QList &list) +{ + GConfValueType result = GCONF_VALUE_INVALID; + + Q_FOREACH (const QVariant &elt, list) { + GConfValueType elt_type = primitiveType (elt); + + if (elt_type == GCONF_VALUE_INVALID) + return GCONF_VALUE_INVALID; + + if (result == GCONF_VALUE_INVALID) + result = elt_type; + else if (result != elt_type) + return GCONF_VALUE_INVALID; + } + + if (result == GCONF_VALUE_INVALID) + return GCONF_VALUE_STRING; // empty list. + else + return result; +} + +static int convertValue(const QVariant &src, GConfValue **valp) +{ + GConfValue *v; + + switch(src.type()) { + case QVariant::Invalid: + v = NULL; + break; + case QVariant::Bool: + v = gconf_value_new (GCONF_VALUE_BOOL); + gconf_value_set_bool (v, src.toBool()); + break; + case QVariant::Int: + v = gconf_value_new (GCONF_VALUE_INT); + gconf_value_set_int (v, src.toInt()); + break; + case QVariant::Double: + v = gconf_value_new (GCONF_VALUE_FLOAT); + gconf_value_set_float (v, src.toDouble()); + break; + case QVariant::String: + v = convertString(src.toString()); + break; + case QVariant::StringList: + { + GSList *elts = NULL; + v = gconf_value_new(GCONF_VALUE_LIST); + gconf_value_set_list_type(v, GCONF_VALUE_STRING); + Q_FOREACH (const QString &str, src.toStringList()) + elts = g_slist_prepend(elts, convertString(str)); + gconf_value_set_list_nocopy(v, g_slist_reverse(elts)); + break; + } + case QVariant::List: + { + GConfValueType elt_type = uniformType(src.toList()); + if (elt_type == GCONF_VALUE_INVALID) + v = NULL; + else + { + GSList *elts = NULL; + v = gconf_value_new(GCONF_VALUE_LIST); + gconf_value_set_list_type(v, elt_type); + Q_FOREACH (const QVariant &elt, src.toList()) + { + GConfValue *val = NULL; + convertValue(elt, &val); // guaranteed to succeed. + elts = g_slist_prepend(elts, val); + } + gconf_value_set_list_nocopy(v, g_slist_reverse(elts)); + } + break; + } + default: + return 0; + } + + *valp = v; + return 1; +} + +void GConfItemPrivate::notify_trampoline (GConfClient*, + guint, + GConfEntry *entry, + gpointer data) +{ + GConfItem *item = (GConfItem *)data; + + item->update_value (true, entry->key, convertValue(entry->value)); +} + +void GConfItem::update_value (bool emit_signal, const QString& key, const QVariant& value) +{ + QVariant new_value; + + if (emit_signal) { + subtreeChanged(key, value); + } +} + +QString GConfItem::root() const +{ + return priv->root; +} + +QVariant GConfItem::value(const QString &subKey) const +{ + QVariant new_value; + withClient(client) { + GError *error = NULL; + QByteArray k = convertKey(priv->root + '/' + subKey); + GConfValue *v = gconf_client_get(client, k.data(), &error); + + if (error) { + qWarning() << error->message; + g_error_free (error); + new_value = QVariant(); + } else { + new_value = convertValue(v); + if (v) + gconf_value_free(v); + } + } + return new_value; +} + +void GConfItem::set(const QString &subKey, const QVariant &val) +{ + withClient(client) { + QByteArray k = convertKey(priv->root + '/' + subKey); + GConfValue *v; + if (convertValue(val, &v)) { + GError *error = NULL; + + if (v) { + gconf_client_set(client, k.data(), v, &error); + gconf_value_free(v); + } else { + gconf_client_unset(client, k.data(), &error); + } + + if (error) { + qWarning() << error->message; + g_error_free(error); + } + } else { + qWarning() << "Can't store a" << val.typeName(); + } + } +} + +QList GConfItem::listDirs() const +{ + QList children; + + withClient(client) { + QByteArray k = convertKey(priv->root); + GSList *dirs = gconf_client_all_dirs(client, k.data(), NULL); + for (GSList *d = dirs; d; d = d->next) { + children.append(convertKey((char *)d->data)); + g_free (d->data); + } + g_slist_free (dirs); + } + + return children; +} + +QList GConfItem::listEntries() const +{ + QList children; + + withClient(client) { + QByteArray k = convertKey(priv->root); + GSList *entries = gconf_client_all_entries(client, k.data(), NULL); + for (GSList *e = entries; e; e = e->next) { + children.append(convertKey(((GConfEntry *)e->data)->key)); + gconf_entry_free ((GConfEntry *)e->data); + } + g_slist_free (entries); + } + + return children; +} + +GConfItem::GConfItem(const QString &key, QObject *parent) + : QObject (parent), + priv(new GConfItemPrivate) +{ + priv->root = key; + withClient(client) { + QByteArray k = convertKey(priv->root); + gconf_client_add_dir (client, k.data(), GCONF_CLIENT_PRELOAD_ONELEVEL, NULL); + priv->notify_id = gconf_client_notify_add (client, k.data(), + GConfItemPrivate::notify_trampoline, this, + NULL, NULL); + } +} + +GConfItem::~GConfItem() +{ + withClient(client) { + QByteArray k = convertKey(priv->root); + gconf_client_notify_remove (client, priv->notify_id); + gconf_client_remove_dir (client, k.data(), NULL); + } + delete priv; +} diff --git a/plugins/systemvolume/kdeconnect_systemvolume.json b/plugins/systemvolume/kdeconnect_systemvolume.json new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/kdeconnect_systemvolume.json @@ -0,0 +1,28 @@ +{ + "Encoding": "UTF-8", + "KPlugin": { + "Authors": [ + { + "Email": "nicolas.fella@gmx.de", + "Name": "Nicolas Fella" + } + ], + "Description": "Control the system volume from your phone", + "EnabledByDefault": true, + "Icon": "audio-volume-high", + "Id": "kdeconnect_systemvolume", + "License": "GPL", + "Name": "System volume", + "ServiceTypes": [ + "KdeConnect/Plugin" + ], + "Version": "0.1", + "Website": "http://albertvaka.wordpress.com" + }, + "X-KdeConnect-OutgoingPacketType": [ + "kdeconnect.systemvolume" + ], + "X-KdeConnect-SupportedPacketType": [ + "kdeconnect.systemvolume" + ] +} diff --git a/plugins/systemvolume/maps.h b/plugins/systemvolume/maps.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/maps.h @@ -0,0 +1,175 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef MAPS_H +#define MAPS_H + +#include "debug.h" +#include +#include + +#include +#include + +namespace QPulseAudio +{ + +// Used for typedefs. +class Card; +class Client; +class Sink; +class SinkInput; +class Source; +class SourceOutput; +class StreamRestore; +class Module; + +/** + * @see MapBase + * This class is nothing more than the QObject base since moc cannot handle + * templates. + */ +class MapBaseQObject : public QObject +{ + Q_OBJECT + +public: + virtual int count() const = 0; + virtual QObject *objectAt(int index) const = 0; + virtual int indexOfObject(QObject *object) const = 0; + +signals: + void added(int index); + void removed(int index); +}; + +/** + * Maps a specific index to a specific object pointer. + * This is used to give the unique arbitrary PulseAudio index of a PulseObject a + * serialized list index. Namely it enables us to translate a discrete list + * index to a pulse index to an object, and any permutation thereof. + */ +template +class MapBase : public MapBaseQObject +{ +public: + virtual ~MapBase() {} + + const QMap &data() const { return m_data; } + + int count() const Q_DECL_OVERRIDE + { + return m_data.count(); + } + + int indexOfObject(QObject *object) const Q_DECL_OVERRIDE + { + int index = 0; + QMapIterator it(m_data); + while (it.hasNext()) { + it.next(); + if (it.value() == object) { + return index; + } + index++; + } + return -1; + } + + QObject *objectAt(int index) const Q_DECL_OVERRIDE + { + return (m_data.constBegin() + index).value(); + } + + void reset() + { + while (!m_data.isEmpty()) { + removeEntry(m_data.lastKey()); + } + m_pendingRemovals.clear(); + } + + void insert(Type *object) + { + Q_ASSERT(!m_data.contains(object->index())); + + m_data.insert(object->index(), object); + + const int modelIndex = m_data.keys().indexOf(object->index()); + Q_ASSERT(modelIndex >= 0); + emit added(modelIndex); + } + + // Context is passed in as parent because context needs to include the maps + // so we'd cause a circular dep if we were to try to use the instance here. + // Plus that's weird separation anyway. + void updateEntry(const PAInfo *info, QObject *parent) + { + Q_ASSERT(info); + + if (m_pendingRemovals.remove(info->index)) { + // Was already removed again. + return; + } + + const bool isNew = !m_data.contains(info->index); + + auto *obj = m_data.value(info->index, nullptr); + if (!obj) { + obj = new Type(parent); + } + obj->update(info); + m_data.insert(info->index, obj); + + if (isNew) { + const int modelIndex = m_data.keys().indexOf(info->index); + Q_ASSERT(modelIndex >= 0); + emit added(modelIndex); + } + } + + void removeEntry(quint32 index) + { + if (!m_data.contains(index)) { + m_pendingRemovals.insert(index); + } else { + const int modelIndex = m_data.keys().indexOf(index); + delete m_data.take(index); + emit removed(modelIndex); + } + } + +protected: + QMap m_data; + QSet m_pendingRemovals; +}; + +typedef MapBase SinkMap; +typedef MapBase SinkInputMap; +typedef MapBase SourceMap; +typedef MapBase SourceOutputMap; +typedef MapBase ClientMap; +typedef MapBase CardMap; +typedef MapBase ModuleMap; +typedef MapBase StreamRestoreMap; + +} // QPulseAudio + +#endif // MAPS_H diff --git a/plugins/systemvolume/maps.cpp b/plugins/systemvolume/maps.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/maps.cpp @@ -0,0 +1,21 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "maps.h" diff --git a/plugins/systemvolume/module.h b/plugins/systemvolume/module.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/module.h @@ -0,0 +1,59 @@ +/* + Copyright 2017 David Rosca + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef MODULE_H +#define MODULE_H + +#include + +#include +#include + +#include "pulseobject.h" + +namespace QPulseAudio +{ + +class Module : public PulseObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString argument READ argument NOTIFY argumentChanged) + +public: + Module(QObject *parent); + + void update(const pa_module_info *info); + + QString name() const; + QString argument() const; + +signals: + void nameChanged(); + void argumentChanged(); + +private: + QString m_name; + QString m_argument; +}; + +} // QPulseAudio + +#endif // MODULE_H diff --git a/plugins/systemvolume/module.cpp b/plugins/systemvolume/module.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/module.cpp @@ -0,0 +1,61 @@ +/* + Copyright 2017 David Rosca + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "module.h" + +#include "debug.h" + +#include "context.h" + +namespace QPulseAudio +{ + +Module::Module(QObject *parent) + : PulseObject(parent) +{ +} + +void Module::update(const pa_module_info *info) +{ + updatePulseObject(info); + + const QString infoName = QString::fromUtf8(info->name); + if (m_name != infoName) { + m_name = infoName; + emit nameChanged(); + } + const QString infoArgument = QString::fromUtf8(info->argument); + if (m_argument != infoArgument) { + m_argument = infoArgument; + emit argumentChanged(); + } +} + +QString Module::name() const +{ + return m_name; +} + +QString Module::argument() const +{ + return m_argument; +} + +} // QPulseAudio diff --git a/plugins/systemvolume/modulemanager.h b/plugins/systemvolume/modulemanager.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/modulemanager.h @@ -0,0 +1,69 @@ +/* + Copyright 2016 David Edmundson + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef MODULEMANAGER_H +#define MODULEMANAGER_H + +#include + +#include + +#include "context.h" +// Properties need fully qualified classes even with pointers. +#include "client.h" + +class GConfItem; + +namespace QPulseAudio +{ +class GConfModule; + +class ModuleManager : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool combineSinks READ combineSinks WRITE setCombineSinks NOTIFY combineSinksChanged) + Q_PROPERTY(bool switchOnConnect READ switchOnConnect WRITE setSwitchOnConnect NOTIFY switchOnConnectChanged) + Q_PROPERTY(QStringList loadedModules READ loadedModules NOTIFY loadedModulesChanged) +public: + ModuleManager(QObject *parent = nullptr); + ~ModuleManager(); + bool combineSinks() const; + void setCombineSinks(bool combineSinks); + bool switchOnConnect() const; + void setSwitchOnConnect(bool switchOnConnect); + QStringList loadedModules() const; + +Q_SIGNALS: + void combineSinksChanged(); + void switchOnConnectChanged(); + void loadedModulesChanged(); + +private: + void updateLoadedModules(); + + GConfModule *m_combineSinks; + GConfModule *m_switchOnConnect; + GConfModule *m_deviceManager; + QStringList m_loadedModules; +}; + +} // QPulseAudio + +#endif // STREAM_H diff --git a/plugins/systemvolume/modulemanager.cpp b/plugins/systemvolume/modulemanager.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/modulemanager.cpp @@ -0,0 +1,138 @@ +/* + Copyright 2016 David Edmundson + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + + +#include "modulemanager.h" +#include "module.h" + +#define PA_GCONF_ROOT "/system/pulseaudio" +#define PA_GCONF_PATH_MODULES PA_GCONF_ROOT"/modules" + +#include "gconfitem.h" + +#include + +namespace QPulseAudio +{ + +class GConfModule : public GConfItem +{ + Q_OBJECT +public: + GConfModule(const QString &configName, const QString &moduleName, QObject *parent); + bool isEnabled() const; + void setEnabled(bool enabled, const QVariant &args=QVariant()); +private: + QString m_moduleName; +}; + +GConfModule::GConfModule(const QString &configName, const QString &moduleName, QObject *parent) : + GConfItem(QStringLiteral(PA_GCONF_PATH_MODULES"/") + configName, parent), + m_moduleName(moduleName) +{ +} + +bool GConfModule::isEnabled() const +{ + return value(QStringLiteral("enabled")).toBool(); +} + +void GConfModule::setEnabled(bool enabled, const QVariant &args) +{ + set(QStringLiteral("locked"), true); + + if (enabled) { + set(QStringLiteral("name0"), m_moduleName); + set(QStringLiteral("args0"), args); + set(QStringLiteral("enabled"), true); + } else { + set(QStringLiteral("enabled"), false); + } + set(QStringLiteral("locked"), false); +} + + +ModuleManager::ModuleManager(QObject *parent) : + QObject(parent), + m_combineSinks(new GConfModule(QStringLiteral("combine"), QStringLiteral("module-combine"), this)), + m_switchOnConnect(new GConfModule(QStringLiteral("switch-on-connect"), QStringLiteral("module-switch-on-connect"), this)), + m_deviceManager(new GConfModule(QStringLiteral("device-manager"), QStringLiteral("module-device-manager"), this)) +{ + connect(m_combineSinks, &GConfItem::subtreeChanged, this, &ModuleManager::combineSinksChanged); + connect(m_switchOnConnect, &GConfItem::subtreeChanged, this, &ModuleManager::switchOnConnectChanged); + connect(m_deviceManager, &GConfItem::subtreeChanged, this, &ModuleManager::switchOnConnectChanged); + + QTimer *updateModulesTimer = new QTimer(this); + updateModulesTimer->setInterval(500); + updateModulesTimer->setSingleShot(true); + connect(updateModulesTimer, &QTimer::timeout, this, &ModuleManager::updateLoadedModules); + connect(&Context::instance()->modules(), &MapBaseQObject::added, updateModulesTimer, static_cast(&QTimer::start)); + connect(&Context::instance()->modules(), &MapBaseQObject::removed, updateModulesTimer, static_cast(&QTimer::start)); + updateLoadedModules(); +} + +ModuleManager::~ModuleManager() +{ +}; + +bool ModuleManager::combineSinks() const +{ + return m_combineSinks->isEnabled(); +} + +void ModuleManager::setCombineSinks(bool combineSinks) +{ + m_combineSinks->setEnabled(combineSinks); +} + +bool ModuleManager::switchOnConnect() const +{ + //switch on connect and device-manager do the same task. Only one should be enabled + + //Note on the first run m_deviceManager will appear to be disabled even though it's actually running + //because there is no gconf entry, however m_switchOnConnect will only exist if set by Plasma PA + //hence only check this entry + return m_switchOnConnect->isEnabled() ; +} + +void ModuleManager::setSwitchOnConnect(bool switchOnConnect) +{ + m_deviceManager->setEnabled(!switchOnConnect); + m_switchOnConnect->setEnabled(switchOnConnect); +} + +QStringList ModuleManager::loadedModules() const +{ + return m_loadedModules; +} + +void ModuleManager::updateLoadedModules() +{ + m_loadedModules.clear(); + const auto modules = Context::instance()->modules().data(); + for (Module *module : modules) { + m_loadedModules.append(module->name()); + } + Q_EMIT loadedModulesChanged(); +} + +} + +#include "modulemanager.moc" diff --git a/plugins/systemvolume/operation.h b/plugins/systemvolume/operation.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/operation.h @@ -0,0 +1,71 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef OPERATION_H +#define OPERATION_H + +#include + +namespace QPulseAudio +{ + +/** + * @brief The PAOperation class + * Helps with management of pa_operations. pa_operations need to be expicitly + * unref'd after use, so this class is essentially a fancy scoping helper where + * destruction of an instance would also unref the held operation (if there is + * one). + */ +class PAOperation +{ +public: + /** + * @brief PAOperation + * @param operation operation to manage the scope of + */ + PAOperation(pa_operation *operation = nullptr); + ~PAOperation(); + + PAOperation &operator =(pa_operation *operation); + + /** + * @brief operator ! + * @return whether or not there is an operation pointer + */ + bool operator !(); + + /** + * @brief operator bool representing whether there is an operation + */ + operator bool(); + + /** + * @brief operator * + * @return pointer to internal pa_operation object + */ + pa_operation *&operator *(); + +private: + pa_operation *m_operation; +}; + +} // QPulseAudio + +#endif // OPERATION_H diff --git a/plugins/systemvolume/operation.cpp b/plugins/systemvolume/operation.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/operation.cpp @@ -0,0 +1,59 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "operation.h" + +namespace QPulseAudio +{ + +PAOperation::PAOperation(pa_operation *operation) + : m_operation(operation) +{ +} + +PAOperation::~PAOperation() +{ + if (m_operation) { + pa_operation_unref(m_operation); + } +} + +PAOperation &PAOperation::operator =(pa_operation *operation) +{ + m_operation = operation; + return *this; +} + +bool PAOperation::operator !() +{ + return !m_operation; +} + +pa_operation *&PAOperation::operator *() +{ + return m_operation; +} + +PAOperation::operator bool() +{ + return m_operation; +} + +} // QPulseAudio diff --git a/plugins/systemvolume/osdservice.h b/plugins/systemvolume/osdservice.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/osdservice.h @@ -0,0 +1,98 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -m -c OsdServiceInterface -p osdservice /home/nico/workspace/plasma-pa/src/qml/dbus/osdService.xml + * + * qdbusxml2cpp is Copyright (C) 2017 The Qt Company Ltd. + * + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + +#ifndef OSDSERVICE_H +#define OSDSERVICE_H + +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Proxy class for interface org.kde.osdService + */ +class OsdServiceInterface: public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char *staticInterfaceName() + { return "org.kde.osdService"; } + +public: + OsdServiceInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = nullptr); + + ~OsdServiceInterface(); + +public Q_SLOTS: // METHODS + inline QDBusPendingReply<> brightnessChanged(int percent) + { + QList argumentList; + argumentList << QVariant::fromValue(percent); + return asyncCallWithArgumentList(QStringLiteral("brightnessChanged"), argumentList); + } + + inline QDBusPendingReply<> kbdLayoutChanged(const QString &layoutName) + { + QList argumentList; + argumentList << QVariant::fromValue(layoutName); + return asyncCallWithArgumentList(QStringLiteral("kbdLayoutChanged"), argumentList); + } + + inline QDBusPendingReply<> keyboardBrightnessChanged(int percent) + { + QList argumentList; + argumentList << QVariant::fromValue(percent); + return asyncCallWithArgumentList(QStringLiteral("keyboardBrightnessChanged"), argumentList); + } + + inline QDBusPendingReply<> mediaPlayerVolumeChanged(int percent, const QString &playerName, const QString &playerIconName) + { + QList argumentList; + argumentList << QVariant::fromValue(percent) << QVariant::fromValue(playerName) << QVariant::fromValue(playerIconName); + return asyncCallWithArgumentList(QStringLiteral("mediaPlayerVolumeChanged"), argumentList); + } + + inline QDBusPendingReply<> microphoneVolumeChanged(int percent) + { + QList argumentList; + argumentList << QVariant::fromValue(percent); + return asyncCallWithArgumentList(QStringLiteral("microphoneVolumeChanged"), argumentList); + } + + inline QDBusPendingReply<> virtualDesktopChanged(const QString ¤tVirtualDesktopName) + { + QList argumentList; + argumentList << QVariant::fromValue(currentVirtualDesktopName); + return asyncCallWithArgumentList(QStringLiteral("virtualDesktopChanged"), argumentList); + } + + inline QDBusPendingReply<> volumeChanged(int percent) + { + QList argumentList; + argumentList << QVariant::fromValue(percent); + return asyncCallWithArgumentList(QStringLiteral("volumeChanged"), argumentList); + } + +Q_SIGNALS: // SIGNALS + void osdProgress(const QString &icon, int percent, const QString &additionalText); + void osdText(const QString &icon, const QString &text); +}; + +namespace org { + namespace kde { + typedef ::OsdServiceInterface osdService; + } +} +#endif diff --git a/plugins/systemvolume/osdservice.cpp b/plugins/systemvolume/osdservice.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/osdservice.cpp @@ -0,0 +1,28 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -m -c OsdServiceInterface -p osdservice /home/nico/workspace/plasma-pa/src/qml/dbus/osdService.xml + * + * qdbusxml2cpp is Copyright (C) 2017 The Qt Company Ltd. + * + * This is an auto-generated file. + * This file may have been hand-edited. Look for HAND-EDIT comments + * before re-generating it. + */ + +#include "osdservice.h" + +/* + * Implementation of interface class OsdServiceInterface + */ + +OsdServiceInterface::OsdServiceInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent) + : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) +{ +} + +OsdServiceInterface::~OsdServiceInterface() +{ +} + + +#include "osdservice.moc" diff --git a/plugins/systemvolume/port.h b/plugins/systemvolume/port.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/port.h @@ -0,0 +1,79 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef PORT_H +#define PORT_H + +#include "profile.h" + +#include + +namespace QPulseAudio +{ + +class Port : public Profile +{ + Q_OBJECT + Q_PROPERTY(Availability availability READ availability NOTIFY availabilityChanged) +public: + enum Availability { + Unknown, + Available, + Unavailable + }; + Q_ENUM(Availability) + + Port(QObject *parent); + virtual ~Port(); + + template + void setInfo(const PAInfo *info) + { + Profile::setInfo(info); + + Availability newAvailability; + switch (info->available) { + case PA_PORT_AVAILABLE_NO: + newAvailability = Unavailable; + break; + case PA_PORT_AVAILABLE_YES: + newAvailability = Available; + break; + default: + newAvailability = Unknown; + } + if (m_availability != newAvailability) { + m_availability = newAvailability; + emit availabilityChanged(); + } + } + + Availability availability() const; + +signals: + void availabilityChanged(); + +private: + Availability m_availability; +}; + +} // QPulseAudio + +#endif // PORT_H diff --git a/plugins/systemvolume/port.cpp b/plugins/systemvolume/port.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/port.cpp @@ -0,0 +1,41 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "port.h" + +namespace QPulseAudio +{ + +Port::Port(QObject *parent) + : Profile(parent) + , m_availability(Unknown) +{ +} + +Port::~Port() +{ +} + +Port::Availability Port::availability() const +{ + return m_availability; +} + +} // QPulseAudio diff --git a/plugins/systemvolume/profile.h b/plugins/systemvolume/profile.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/profile.h @@ -0,0 +1,80 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef PROFILE_H +#define PROFILE_H + +#include +#include + +namespace QPulseAudio +{ + +class Profile : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) + Q_PROPERTY(quint32 priority READ priority NOTIFY priorityChanged) +public: + Profile(QObject *parent); + virtual ~Profile(); + + template + void setInfo(const PAInfo *info) + { + // Description is optional. Name not so much as we need some ID. + Q_ASSERT(info->name); + QString infoName = QString::fromUtf8(info->name); + if (m_name != infoName) { + m_name = infoName; + emit nameChanged(); + } + if (info->description) { + QString infoDescription = QString::fromUtf8(info->description); + if (m_description != infoDescription) { + m_description = infoDescription; + emit descriptionChanged(); + } + } + if (m_priority != info->priority) { + m_priority = info->priority; + emit priorityChanged(); + } + } + + QString name() const; + QString description() const; + quint32 priority() const; + +signals: + void nameChanged(); + void descriptionChanged(); + void priorityChanged(); + +private: + QString m_name; + QString m_description; + quint32 m_priority; +}; + +} // QPulseAudio + +#endif // PROFILE_H diff --git a/plugins/systemvolume/profile.cpp b/plugins/systemvolume/profile.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/profile.cpp @@ -0,0 +1,53 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "profile.h" + +namespace QPulseAudio +{ + +Profile::Profile(QObject *parent) + : QObject(parent) + , m_name() + , m_description() + , m_priority(0) +{ +} + +Profile::~Profile() +{ +} + +QString Profile::name() const +{ + return m_name; +} + +QString Profile::description() const +{ + return m_description; +} + +quint32 Profile::priority() const +{ + return m_priority; +} + +} // QPulseAudio diff --git a/plugins/systemvolume/pulseaudio.h b/plugins/systemvolume/pulseaudio.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/pulseaudio.h @@ -0,0 +1,156 @@ +/* + Copyright 2014-2015 Harald Sitter + Copyright 2016 David Rosca + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef PULSEAUDIO_H +#define PULSEAUDIO_H + +#include + +#include "maps.h" + +namespace QPulseAudio +{ + class Context; + +class AbstractModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum ItemRole { + PulseObjectRole = Qt::UserRole + 1 + }; + + QHash roleNames() const Q_DECL_FINAL; + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_FINAL; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + bool setData(const QModelIndex &index, const QVariant &value, int role) Q_DECL_FINAL; + + Q_INVOKABLE int role(const QByteArray &roleName) const; + +protected: + AbstractModel(const MapBaseQObject *map, QObject *parent); + void initRoleNames(const QMetaObject &qobjectMetaObject); + Context *context() const; + +private slots: + void propertyChanged(); + +private: + void onDataAdded(int index); + void onDataRemoved(int index); + QMetaMethod propertyChangedMetaMethod() const; + + const MapBaseQObject *m_map; + QHash m_roles; + QHash m_objectProperties; + QHash m_signalIndexToProperties; + +private: + // Prevent leaf-classes from default constructing as we want to enforce + // them passing us a context or explicit nullptrs. + AbstractModel() {} +}; + +class CardModel : public AbstractModel +{ + Q_OBJECT +public: + CardModel(QObject *parent = nullptr); +}; + +class SinkModel : public AbstractModel +{ + Q_OBJECT + Q_PROPERTY(QPulseAudio::Sink *defaultSink READ defaultSink NOTIFY defaultSinkChanged) + Q_PROPERTY(QPulseAudio::Sink *preferredSink READ preferredSink NOTIFY preferredSinkChanged) +public: + enum ItemRole { + SortByDefaultRole = PulseObjectRole + 1 + }; + Q_ENUMS(ItemRole) + + SinkModel(QObject *parent = nullptr); + Sink *defaultSink() const; + Sink *preferredSink() const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + +signals: + void defaultSinkChanged(); + void preferredSinkChanged(); + +private: + void sinkAdded(int index); + void sinkRemoved(int index); + void updatePreferredSink(); + Sink *findPreferredSink() const; + + Sink *m_preferredSink; +}; + +class SinkInputModel : public AbstractModel +{ + Q_OBJECT +public: + SinkInputModel(QObject *parent = nullptr); +}; + +class SourceModel : public AbstractModel +{ + Q_OBJECT + Q_PROPERTY(QPulseAudio::Source *defaultSource READ defaultSource NOTIFY defaultSourceChanged) +public: + enum ItemRole { + SortByDefaultRole = PulseObjectRole + 1 + }; + Q_ENUMS(ItemRole) + + SourceModel(QObject *parent = nullptr); + Source *defaultSource() const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + +signals: + void defaultSourceChanged(); +}; + +class SourceOutputModel : public AbstractModel +{ + Q_OBJECT +public: + SourceOutputModel(QObject *parent = nullptr); +}; + +class StreamRestoreModel : public AbstractModel +{ + Q_OBJECT +public: + StreamRestoreModel(QObject *parent = nullptr); +}; + +class ModuleModel : public AbstractModel +{ + Q_OBJECT +public: + ModuleModel(QObject *parent = nullptr); +}; + +} // QPulseAudio + +#endif // PULSEAUDIO_H diff --git a/plugins/systemvolume/pulseaudio.cpp b/plugins/systemvolume/pulseaudio.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/pulseaudio.cpp @@ -0,0 +1,369 @@ +/* + Copyright 2014-2015 Harald Sitter + Copyright 2016 David Rosca + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "pulseaudio.h" + +#include "debug.h" +#include "card.h" +#include "sink.h" +#include "sinkinput.h" +#include "source.h" +#include "sourceoutput.h" +#include "server.h" +#include "streamrestore.h" +#include "module.h" + +#include + +namespace QPulseAudio +{ + +AbstractModel::AbstractModel(const MapBaseQObject *map, QObject *parent) + : QAbstractListModel(parent) + , m_map(map) +{ + Context::instance()->ref(); + //deref context after we've deleted this object + //see https://bugs.kde.org/show_bug.cgi?id=371215 + connect(this, &QObject::destroyed, []() { + Context::instance()->unref(); + }); + + connect(m_map, &MapBaseQObject::added, this, &AbstractModel::onDataAdded); + connect(m_map, &MapBaseQObject::removed, this, &AbstractModel::onDataRemoved); +} + +QHash AbstractModel::roleNames() const +{ + if (!m_roles.empty()) { + qCDebug(PLASMAPA) << "returning roles" << m_roles; + return m_roles; + } + Q_UNREACHABLE(); + return QHash(); +} + +int AbstractModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_map->count(); +} + +QVariant AbstractModel::data(const QModelIndex &index, int role) const +{ + QObject *data = m_map->objectAt(index.row()); + Q_ASSERT(data); + if (role == PulseObjectRole) { + return QVariant::fromValue(data); + } + int property = m_objectProperties.value(role, -1); + if (property == -1) { + return QVariant(); + } + return data->metaObject()->property(property).read(data); +} + +bool AbstractModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + int propertyIndex = m_objectProperties.value(role, -1); + if (propertyIndex == -1) { + return false; + } + QObject *data = m_map->objectAt(index.row()); + auto property = data->metaObject()->property(propertyIndex); + return property.write(data, value); +} + +int AbstractModel::role(const QByteArray &roleName) const +{ + qCDebug(PLASMAPA) << roleName << m_roles.key(roleName, -1); + return m_roles.key(roleName, -1); +} + +Context *AbstractModel::context() const +{ + return Context::instance(); +} + +void AbstractModel::initRoleNames(const QMetaObject &qobjectMetaObject) +{ + m_roles[PulseObjectRole] = QByteArrayLiteral("PulseObject"); + + QMetaEnum enumerator; + for (int i = 0; i < metaObject()->enumeratorCount(); ++i) { + if (metaObject()->enumerator(i).name() == QLatin1String("ItemRole")) { + enumerator = metaObject()->enumerator(i); + break; + } + } + + for (int i = 0; i < enumerator.keyCount(); ++i) { + // Clip the Role suffix and glue it in the hash. + const int roleLength = 4; + QByteArray key(enumerator.key(i)); + // Enum values must end in Role or the enum is crap + Q_ASSERT(key.right(roleLength) == QByteArrayLiteral("Role")); + key.chop(roleLength); + m_roles[enumerator.value(i)] = key; + } + + int maxEnumValue = -1; + for (auto it = m_roles.constBegin(); it != m_roles.constEnd(); ++it) { + if (it.key() > maxEnumValue) { + maxEnumValue = it.key(); + } + } + Q_ASSERT(maxEnumValue != -1); + auto mo = qobjectMetaObject; + for (int i = 0; i < mo.propertyCount(); ++i) { + QMetaProperty property = mo.property(i); + QString name(property.name()); + name.replace(0, 1, name.at(0).toUpper()); + m_roles[++maxEnumValue] = name.toLatin1(); + m_objectProperties.insert(maxEnumValue, i); + if (!property.hasNotifySignal()) { + continue; + } + m_signalIndexToProperties.insert(property.notifySignalIndex(), i); + } + qCDebug(PLASMAPA) << m_roles; + + // Connect to property changes also with objects already in model + for (int i = 0; i < m_map->count(); ++i) { + onDataAdded(i); + } +} + +void AbstractModel::propertyChanged() +{ + if (!sender() || senderSignalIndex() == -1) { + return; + } + int propertyIndex = m_signalIndexToProperties.value(senderSignalIndex(), -1); + if (propertyIndex == -1) { + return; + } + int role = m_objectProperties.key(propertyIndex, -1); + if (role == -1) { + return; + } + int index = m_map->indexOfObject(sender()); + qCDebug(PLASMAPA) << "PROPERTY CHANGED (" << index << ") :: " << role << roleNames().value(role); + emit dataChanged(createIndex(index, 0), createIndex(index, 0), {role}); +} + +void AbstractModel::onDataAdded(int index) +{ + beginInsertRows(QModelIndex(), index, index); + QObject *data = m_map->objectAt(index); + const QMetaObject *mo = data->metaObject(); + // We have all the data changed notify signals already stored + auto keys = m_signalIndexToProperties.keys(); + foreach (int index, keys) { + QMetaMethod meth = mo->method(index); + connect(data, meth, this, propertyChangedMetaMethod()); + } + endInsertRows(); +} + +void AbstractModel::onDataRemoved(int index) +{ + beginRemoveRows(QModelIndex(), index, index); + endRemoveRows(); +} + +QMetaMethod AbstractModel::propertyChangedMetaMethod() const +{ + auto mo = metaObject(); + int methodIndex = mo->indexOfMethod("propertyChanged()"); + if (methodIndex == -1) { + return QMetaMethod(); + } + return mo->method(methodIndex); +} + +SinkModel::SinkModel(QObject *parent) + : AbstractModel(&context()->sinks(), parent) + , m_preferredSink(nullptr) +{ + initRoleNames(Sink::staticMetaObject); + + for (int i = 0; i < context()->sinks().count(); ++i) { + sinkAdded(i); + } + + connect(&context()->sinks(), &MapBaseQObject::added, this, &SinkModel::sinkAdded); + connect(&context()->sinks(), &MapBaseQObject::removed, this, &SinkModel::sinkRemoved); + + connect(context()->server(), &Server::defaultSinkChanged, this, [this]() { + updatePreferredSink(); + emit defaultSinkChanged(); + }); +} + +Sink *SinkModel::defaultSink() const +{ + return context()->server()->defaultSink(); +} + +Sink *SinkModel::preferredSink() const +{ + return m_preferredSink; +} + +QVariant SinkModel::data(const QModelIndex &index, int role) const +{ + if (role == SortByDefaultRole) { + // Workaround QTBUG-1548 + const QString pulseIndex = data(index, AbstractModel::role(QByteArrayLiteral("Index"))).toString(); + const QString defaultDevice = data(index, AbstractModel::role(QByteArrayLiteral("Default"))).toString(); + return defaultDevice + pulseIndex; + } + return AbstractModel::data(index, role); +} + +void SinkModel::sinkAdded(int index) +{ + Q_ASSERT(qobject_cast(context()->sinks().objectAt(index))); + Sink *sink = static_cast(context()->sinks().objectAt(index)); + connect(sink, &Sink::stateChanged, this, &SinkModel::updatePreferredSink); + + updatePreferredSink(); +} + +void SinkModel::sinkRemoved(int index) +{ + Q_UNUSED(index); + + updatePreferredSink(); +} + +void SinkModel::updatePreferredSink() +{ + Sink *sink = findPreferredSink(); + + if (sink != m_preferredSink) { + qCDebug(PLASMAPA) << "Changing preferred sink to" << sink << (sink ? sink->name() : ""); + m_preferredSink = sink; + emit preferredSinkChanged(); + } +} + +Sink *SinkModel::findPreferredSink() const +{ + const auto &sinks = context()->sinks(); + + // Only one sink is the preferred one + if (sinks.count() == 1) { + return static_cast(sinks.objectAt(0)); + } + + auto lookForState = [this](Device::State state) { + Sink *ret = nullptr; + QMapIterator it(context()->sinks().data()); + while (it.hasNext()) { + it.next(); + if (it.value()->state() != state) { + continue; + } + if (!ret) { + ret = it.value(); + } else if (it.value() == defaultSink()) { + ret = it.value(); + break; + } + } + return ret; + }; + + Sink *preferred = nullptr; + + // Look for playing sinks + prefer default sink + preferred = lookForState(Device::RunningState); + if (preferred) { + return preferred; + } + + // Look for idle sinks + prefer default sink + preferred = lookForState(Device::IdleState); + if (preferred) { + return preferred; + } + + // Fallback to default sink + return defaultSink(); +} + +SourceModel::SourceModel(QObject *parent) + : AbstractModel(&context()->sources(), parent) +{ + initRoleNames(Source::staticMetaObject); + + connect(context()->server(), &Server::defaultSourceChanged, this, &SourceModel::defaultSourceChanged); +} + +Source *SourceModel::defaultSource() const +{ + return context()->server()->defaultSource(); +} + +QVariant SourceModel::data(const QModelIndex &index, int role) const +{ + if (role == SortByDefaultRole) { + // Workaround QTBUG-1548 + const QString pulseIndex = data(index, AbstractModel::role(QByteArrayLiteral("Index"))).toString(); + const QString defaultDevice = data(index, AbstractModel::role(QByteArrayLiteral("Default"))).toString(); + return defaultDevice + pulseIndex; + } + return AbstractModel::data(index, role); +} + +SinkInputModel::SinkInputModel(QObject *parent) + : AbstractModel(&context()->sinkInputs(), parent) +{ + initRoleNames(SinkInput::staticMetaObject); +} + +SourceOutputModel::SourceOutputModel(QObject *parent) + : AbstractModel(&context()->sourceOutputs(), parent) +{ + initRoleNames(SourceOutput::staticMetaObject); +} + +CardModel::CardModel(QObject *parent) + : AbstractModel(&context()->cards(), parent) +{ + initRoleNames(Card::staticMetaObject); +} + +StreamRestoreModel::StreamRestoreModel(QObject *parent) + : AbstractModel(&context()->streamRestores(), parent) +{ + initRoleNames(StreamRestore::staticMetaObject); +} + +ModuleModel::ModuleModel(QObject *parent) + : AbstractModel(&context()->modules(), parent) +{ + initRoleNames(Module::staticMetaObject); +} + +} // QPulseAudio diff --git a/plugins/systemvolume/pulseobject.h b/plugins/systemvolume/pulseobject.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/pulseobject.h @@ -0,0 +1,83 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef PULSEOBJECT_H +#define PULSEOBJECT_H + +#include "debug.h" +#include + +#include + +namespace QPulseAudio +{ + +class Context; + +class PulseObject : public QObject +{ + Q_OBJECT + Q_PROPERTY(quint32 index READ index CONSTANT) + Q_PROPERTY(QString iconName READ iconName CONSTANT) + Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged) +public: + template + void updatePulseObject(PAInfo *info) + { + m_index = info->index; + + m_properties.clear(); + void *it = nullptr; + while (const char *key = pa_proplist_iterate(info->proplist, &it)) { + Q_ASSERT(key); + const char *value = pa_proplist_gets(info->proplist, key); + if (!value) { + qCDebug(PLASMAPA) << "property" << key << "not a string"; + continue; + } + Q_ASSERT(value); + m_properties.insert(QString::fromUtf8(key), QString::fromUtf8(value)); + } + emit propertiesChanged(); + } + + quint32 index() const; + QString iconName() const; + QVariantMap properties() const; + +signals: + void propertiesChanged(); + +protected: + PulseObject(QObject *parent); + virtual ~PulseObject(); + + Context *context() const; + quint32 m_index; + QVariantMap m_properties; + +private: + // Ensure that we get properly parented. + PulseObject(); +}; + +} // QPulseAudio + +#endif // PULSEOBJECT_H diff --git a/plugins/systemvolume/pulseobject.cpp b/plugins/systemvolume/pulseobject.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/pulseobject.cpp @@ -0,0 +1,90 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "pulseobject.h" + +#include "context.h" + +#include + +namespace QPulseAudio +{ + +PulseObject::PulseObject(QObject *parent) + : QObject(parent) + , m_index(0) +{ +} + +PulseObject::~PulseObject() +{ +} + +Context *PulseObject::context() const +{ + return Context::instance(); +} + +uint32_t PulseObject::index() const +{ + return m_index; +} + +QString PulseObject::iconName() const +{ + QString name = m_properties.value(QStringLiteral("device.icon_name")).toString(); + if (!name.isEmpty() && QIcon::hasThemeIcon(name)) { + return name; + } + + name = m_properties.value(QStringLiteral("media.icon_name")).toString(); + if (!name.isEmpty() && QIcon::hasThemeIcon(name)) { + return name; + } + + name = m_properties.value(QStringLiteral("window.icon_name")).toString(); + if (!name.isEmpty() && QIcon::hasThemeIcon(name)) { + return name; + } + + name = m_properties.value(QStringLiteral("application.icon_name")).toString(); + if (!name.isEmpty() && QIcon::hasThemeIcon(name)) { + return name; + } + + name = m_properties.value(QStringLiteral("application.process.binary")).toString(); + if (!name.isEmpty() && QIcon::hasThemeIcon(name)) { + return name; + } + + name = property("name").toString(); + if (!name.isEmpty() && QIcon::hasThemeIcon(name)) { + return name; + } + + return QString(); +} + +QVariantMap PulseObject::properties() const +{ + return m_properties; +} + +} // QPulseAudio diff --git a/plugins/systemvolume/server.h b/plugins/systemvolume/server.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/server.h @@ -0,0 +1,65 @@ +/* + Copyright 2016 David Rosca + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef SERVER_H +#define SERVER_H + +#include +#include + +namespace QPulseAudio +{ + +class Sink; +class Source; +class Context; + +class Server : public QObject +{ + Q_OBJECT + +public: + explicit Server(Context *context); + + Sink *defaultSink() const; + void setDefaultSink(Sink *sink); + + Source *defaultSource() const; + void setDefaultSource(Source *source); + + void reset(); + void update(const pa_server_info *info); + +signals: + void defaultSinkChanged(Sink *sink); + void defaultSourceChanged(Source *source); + +private: + void updateDefaultDevices(); + + QString m_defaultSinkName; + QString m_defaultSourceName; + Sink *m_defaultSink; + Source *m_defaultSource; +}; + +} // QPulseAudio + +#endif // CONTEXT_H diff --git a/plugins/systemvolume/server.cpp b/plugins/systemvolume/server.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/server.cpp @@ -0,0 +1,123 @@ +/* + Copyright 2016 David Rosca + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "server.h" +#include "context.h" +#include "sink.h" +#include "source.h" +#include "debug.h" + +namespace QPulseAudio +{ + +Server::Server(Context *context) + : QObject(context) + , m_defaultSink(nullptr) + , m_defaultSource(nullptr) +{ + Q_ASSERT(context); + + connect(&context->sinks(), &MapBaseQObject::added, this, &Server::updateDefaultDevices); + connect(&context->sinks(), &MapBaseQObject::removed, this, &Server::updateDefaultDevices); + connect(&context->sources(), &MapBaseQObject::added, this, &Server::updateDefaultDevices); + connect(&context->sources(), &MapBaseQObject::removed, this, &Server::updateDefaultDevices); +} + +Sink *Server::defaultSink() const +{ + return m_defaultSink; +} + +void Server::setDefaultSink(Sink *sink) +{ + Q_ASSERT(sink); + Context::instance()->setDefaultSink(sink->name()); +} + +Source *Server::defaultSource() const +{ + return m_defaultSource; +} + +void Server::setDefaultSource(Source *source) +{ + Q_ASSERT(source); + Context::instance()->setDefaultSource(source->name()); +} + +void Server::reset() +{ + if (m_defaultSink) { + m_defaultSink = nullptr; + emit defaultSinkChanged(m_defaultSink); + } + + if (m_defaultSource) { + m_defaultSource = nullptr; + emit defaultSourceChanged(m_defaultSource); + } +} + +void Server::update(const pa_server_info *info) +{ + m_defaultSinkName = QString::fromUtf8(info->default_sink_name); + m_defaultSourceName = QString::fromUtf8(info->default_source_name); + + updateDefaultDevices(); +} + +template +static Type *findByName(const Map &map, const QString &name) +{ + Type *out = nullptr; + if (name.isEmpty()) { + return out; + } + QMapIterator it(map); + while (it.hasNext()) { + it.next(); + out = it.value(); + if (out->name() == name) { + return out; + } + } + qCWarning(PLASMAPA) << "No object for name" << name; + return out; +} + +void Server::updateDefaultDevices() +{ + Sink *sink = findByName(Context::instance()->sinks().data(), m_defaultSinkName); + Source *source = findByName(Context::instance()->sources().data(), m_defaultSourceName); + + if (m_defaultSink != sink) { + qCDebug(PLASMAPA) << "Default sink changed" << sink; + m_defaultSink = sink; + emit defaultSinkChanged(m_defaultSink); + } + + if (m_defaultSource != source) { + qCDebug(PLASMAPA) << "Default source changed" << source; + m_defaultSource = source; + emit defaultSourceChanged(m_defaultSource); + } +} + +} // QPulseAudio diff --git a/plugins/systemvolume/sink.h b/plugins/systemvolume/sink.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/sink.h @@ -0,0 +1,47 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef SINK_H +#define SINK_H + +#include "device.h" + +namespace QPulseAudio +{ + +class Sink : public Device +{ + Q_OBJECT +public: + Sink(QObject *parent); + + void update(const pa_sink_info *info); + void setVolume(qint64 volume) Q_DECL_OVERRIDE; + void setMuted(bool muted) Q_DECL_OVERRIDE; + void setActivePortIndex(quint32 port_index) Q_DECL_OVERRIDE; + void setChannelVolume(int channel, qint64 volume) Q_DECL_OVERRIDE; + + bool isDefault() const Q_DECL_OVERRIDE; + void setDefault(bool enable) Q_DECL_OVERRIDE; +}; + +} // QPulseAudio + +#endif // SINK_H diff --git a/plugins/systemvolume/sink.cpp b/plugins/systemvolume/sink.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/sink.cpp @@ -0,0 +1,77 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "sink.h" + +#include "context.h" +#include "server.h" + +namespace QPulseAudio +{ + +Sink::Sink(QObject *parent) + : Device(parent) +{ + connect(context()->server(), &Server::defaultSinkChanged, this, &Sink::defaultChanged); +} + +void Sink::update(const pa_sink_info *info) +{ + updateDevice(info); +} + +void Sink::setVolume(qint64 volume) +{ + context()->setGenericVolume(index(), -1, volume, cvolume(), &pa_context_set_sink_volume_by_index); +} + +void Sink::setMuted(bool muted) +{ + context()->setGenericMute(m_index, muted, &pa_context_set_sink_mute_by_index); +} + +void Sink::setActivePortIndex(quint32 port_index) +{ + Port *port = qobject_cast(ports().at(port_index)); + if (!port) { + qCWarning(PLASMAPA) << "invalid port set request" << port_index; + return; + } + context()->setGenericPort(index(), port->name(), &pa_context_set_sink_port_by_index); +} + +void Sink::setChannelVolume(int channel, qint64 volume) +{ + context()->setGenericVolume(index(), channel, volume, cvolume(), &pa_context_set_sink_volume_by_index); +} + +bool Sink::isDefault() const +{ + return context()->server()->defaultSink() == this; +} + +void Sink::setDefault(bool enable) +{ + if (!isDefault() && enable) { + context()->server()->setDefaultSink(this); + } +} + +} // QPulseAudio diff --git a/plugins/systemvolume/sinkinput.h b/plugins/systemvolume/sinkinput.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/sinkinput.h @@ -0,0 +1,47 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef SINKINPUT_H +#define SINKINPUT_H + +#include "stream.h" + +namespace QPulseAudio +{ + +class SinkInput : public Stream +{ + Q_OBJECT +public: + SinkInput(QObject *parent); + + void update(const pa_sink_input_info *info); + + void setSinkIndex(quint32 sinkIndex); + + void setVolume(qint64 volume) Q_DECL_OVERRIDE; + void setMuted(bool muted) Q_DECL_OVERRIDE; + void setChannelVolume(int channel, qint64 volume) Q_DECL_OVERRIDE; + void setDeviceIndex(quint32 deviceIndex) Q_DECL_OVERRIDE; +}; + +} // QPulseAudio + +#endif // SINKINPUT_H diff --git a/plugins/systemvolume/sinkinput.cpp b/plugins/systemvolume/sinkinput.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/sinkinput.cpp @@ -0,0 +1,62 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "sinkinput.h" + +#include "context.h" + +namespace QPulseAudio +{ + +SinkInput::SinkInput(QObject *parent) + : Stream(parent) +{ +} + +void SinkInput::update(const pa_sink_input_info *info) +{ + updateStream(info); + if (m_deviceIndex != info->sink) { + m_deviceIndex = info->sink; + emit deviceIndexChanged(); + } +} + +void SinkInput::setDeviceIndex(quint32 deviceIndex) +{ + context()->setGenericDeviceForStream(index(), deviceIndex, &pa_context_move_sink_input_by_index); +} + +void SinkInput::setVolume(qint64 volume) +{ + context()->setGenericVolume(index(), -1, volume, cvolume(), &pa_context_set_sink_input_volume); +} + +void SinkInput::setMuted(bool muted) +{ + context()->setGenericMute(index(), muted, &pa_context_set_sink_input_mute); +} + +void SinkInput::setChannelVolume(int channel, qint64 volume) +{ + context()->setGenericVolume(index(), channel, volume, cvolume(), &pa_context_set_sink_input_volume); +} + +} // QPulseAudio diff --git a/plugins/systemvolume/source.h b/plugins/systemvolume/source.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/source.h @@ -0,0 +1,47 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef SOURCE_H +#define SOURCE_H + +#include "device.h" + +namespace QPulseAudio +{ + +class Source : public Device +{ + Q_OBJECT +public: + Source(QObject *parent); + + void update(const pa_source_info *info); + void setVolume(qint64 volume) Q_DECL_OVERRIDE; + void setMuted(bool muted) Q_DECL_OVERRIDE; + void setActivePortIndex(quint32 port_index) Q_DECL_OVERRIDE; + void setChannelVolume(int channel, qint64 volume) Q_DECL_OVERRIDE; + + bool isDefault() const Q_DECL_OVERRIDE; + void setDefault(bool enable) Q_DECL_OVERRIDE; +}; + +} // QPulseAudio + +#endif // SOURCE_H diff --git a/plugins/systemvolume/source.cpp b/plugins/systemvolume/source.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/source.cpp @@ -0,0 +1,77 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "source.h" + +#include "context.h" +#include "server.h" + +namespace QPulseAudio +{ + +Source::Source(QObject *parent) + : Device(parent) +{ + connect(context()->server(), &Server::defaultSourceChanged, this, &Source::defaultChanged); +} + +void Source::update(const pa_source_info *info) +{ + updateDevice(info); +} + +void Source::setVolume(qint64 volume) +{ + context()->setGenericVolume(index(), -1, volume, cvolume(), &pa_context_set_source_volume_by_index); +} + +void Source::setMuted(bool muted) +{ + context()->setGenericMute(index(), muted, &pa_context_set_source_mute_by_index); +} + +void Source::setActivePortIndex(quint32 port_index) +{ + Port *port = qobject_cast(ports().at(port_index)); + if (!port) { + qCWarning(PLASMAPA) << "invalid port set request" << port_index; + return; + } + context()->setGenericPort(index(), port->name(), &pa_context_set_source_port_by_index); +} + +void Source::setChannelVolume(int channel, qint64 volume) +{ + context()->setGenericVolume(index(), channel, volume, cvolume(), &pa_context_set_source_volume_by_index); +} + +bool Source::isDefault() const +{ + return context()->server()->defaultSource() == this; +} + +void Source::setDefault(bool enable) +{ + if (!isDefault() && enable) { + context()->server()->setDefaultSource(this); + } +} + +} // QPulseAudio diff --git a/plugins/systemvolume/sourceoutput.h b/plugins/systemvolume/sourceoutput.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/sourceoutput.h @@ -0,0 +1,45 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef SOURCEOUTPUT_H +#define SOURCEOUTPUT_H + +#include "stream.h" + +namespace QPulseAudio +{ + +class SourceOutput : public Stream +{ + Q_OBJECT +public: + SourceOutput(QObject *parent); + + void update(const pa_source_output_info *info); + + void setVolume(qint64 volume) Q_DECL_OVERRIDE; + void setMuted(bool muted) Q_DECL_OVERRIDE; + void setChannelVolume(int channel, qint64 volume) Q_DECL_OVERRIDE; + void setDeviceIndex(quint32 deviceIndex) Q_DECL_OVERRIDE; +}; + +} // QPulseAudio + +#endif // SOURCEOUTPUT_H diff --git a/plugins/systemvolume/sourceoutput.cpp b/plugins/systemvolume/sourceoutput.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/sourceoutput.cpp @@ -0,0 +1,62 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "sourceoutput.h" + +#include "context.h" + +namespace QPulseAudio +{ + +SourceOutput::SourceOutput(QObject *parent) + : Stream(parent) +{ +} + +void SourceOutput::update(const pa_source_output_info *info) +{ + updateStream(info); + if (m_deviceIndex != info->source) { + m_deviceIndex = info->source; + emit deviceIndexChanged(); + } +} + +void SourceOutput::setDeviceIndex(quint32 deviceIndex) +{ + context()->setGenericDeviceForStream(index(), deviceIndex, &pa_context_move_source_output_by_index); +} + +void SourceOutput::setVolume(qint64 volume) +{ + context()->setGenericVolume(index(), -1, volume, cvolume(), &pa_context_set_source_output_volume); +} + +void SourceOutput::setMuted(bool muted) +{ + context()->setGenericMute(index(), muted, &pa_context_set_source_output_mute); +} + +void SourceOutput::setChannelVolume(int channel, qint64 volume) +{ + context()->setGenericVolume(index(), channel, volume, cvolume(), &pa_context_set_source_output_volume); +} + +} // QPulseAudio diff --git a/plugins/systemvolume/stream.h b/plugins/systemvolume/stream.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/stream.h @@ -0,0 +1,108 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef STREAM_H +#define STREAM_H + +#include + +#include + +#include "volumeobject.h" +#include "pulseobject.h" + +#include "context.h" +// Properties need fully qualified classes even with pointers. +#include "client.h" + +namespace QPulseAudio +{ + +class Stream : public VolumeObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QPulseAudio::Client *client READ client NOTIFY clientChanged) + Q_PROPERTY(bool virtualStream READ isVirtualStream NOTIFY virtualStreamChanged) + Q_PROPERTY(quint32 deviceIndex READ deviceIndex WRITE setDeviceIndex NOTIFY deviceIndexChanged) + Q_PROPERTY(bool corked READ isCorked NOTIFY corkedChanged) +public: + template + void updateStream(const PAInfo *info) + { + updateVolumeObject(info); + + if (m_name != QString::fromUtf8(info->name)) { + m_name = QString::fromUtf8(info->name); + emit nameChanged(); + } + if (m_hasVolume != info->has_volume) { + m_hasVolume = info->has_volume; + emit hasVolumeChanged(); + } + if (m_volumeWritable != info->volume_writable) { + m_volumeWritable = info->volume_writable; + emit isVolumeWritableChanged(); + } + if (m_clientIndex != info->client) { + m_clientIndex = info->client; + emit clientChanged(); + } + if (m_virtualStream != (info->client == PA_INVALID_INDEX)) { + m_virtualStream = info->client == PA_INVALID_INDEX; + emit virtualStreamChanged(); + } + if (m_corked != info->corked) { + m_corked = info->corked; + emit corkedChanged(); + } + } + + QString name() const; + Client *client() const; + bool isVirtualStream() const; + quint32 deviceIndex() const; + bool isCorked() const; + + virtual void setDeviceIndex(quint32 deviceIndex) = 0; + +signals: + void nameChanged(); + void clientChanged(); + void virtualStreamChanged(); + void deviceIndexChanged(); + void corkedChanged(); + +protected: + Stream(QObject *parent); + virtual ~Stream(); + + quint32 m_deviceIndex; + +private: + QString m_name; + quint32 m_clientIndex; + bool m_virtualStream; + bool m_corked; +}; + +} // QPulseAudio + +#endif // STREAM_H diff --git a/plugins/systemvolume/stream.cpp b/plugins/systemvolume/stream.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/stream.cpp @@ -0,0 +1,66 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "stream.h" + +namespace QPulseAudio +{ + +Stream::Stream(QObject *parent) + : VolumeObject(parent) + , m_deviceIndex(PA_INVALID_INDEX) + , m_clientIndex(PA_INVALID_INDEX) + , m_virtualStream(false) + , m_corked(false) +{ + m_volumeWritable = false; + m_hasVolume = false; +} + +Stream::~Stream() +{ +} + +QString Stream::name() const +{ + return m_name; +} + +Client *Stream::client() const +{ + return context()->clients().data().value(m_clientIndex, nullptr); +} + +bool Stream::isVirtualStream() const +{ + return m_virtualStream; +} + +quint32 Stream::deviceIndex() const +{ + return m_deviceIndex; +} + +bool Stream::isCorked() const +{ + return m_corked; +} + +} // QPulseAudio diff --git a/plugins/systemvolume/streamrestore.h b/plugins/systemvolume/streamrestore.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/streamrestore.h @@ -0,0 +1,100 @@ +/* + Copyright 2016 David Rosca + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef STREAMRESTORE_H +#define STREAMRESTORE_H + +#include "pulseobject.h" + +#include + +namespace QPulseAudio +{ + +class StreamRestore : public PulseObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString device READ device WRITE setDevice NOTIFY deviceChanged) + Q_PROPERTY(qint64 volume READ volume WRITE setVolume NOTIFY volumeChanged) + Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged) + Q_PROPERTY(bool hasVolume READ hasVolume CONSTANT) + Q_PROPERTY(bool volumeWritable READ isVolumeWritable CONSTANT) + Q_PROPERTY(QStringList channels READ channels NOTIFY channelsChanged) + Q_PROPERTY(QList channelVolumes READ channelVolumes NOTIFY channelVolumesChanged) + Q_PROPERTY(quint32 deviceIndex READ deviceIndex WRITE setDeviceIndex NOTIFY deviceIndexChanged) +public: + StreamRestore(quint32 index, const QVariantMap &properties, QObject *parent); + + void update(const pa_ext_stream_restore_info *info); + + QString name() const; + + QString device() const; + void setDevice(const QString &device); + + qint64 volume() const; + void setVolume(qint64 volume); + + bool isMuted() const; + void setMuted(bool muted); + + bool hasVolume() const; + bool isVolumeWritable() const; + + QStringList channels() const; + + QList channelVolumes() const; + + quint32 deviceIndex() const; + void setDeviceIndex(quint32 deviceIndex); + + Q_INVOKABLE void setChannelVolume(int channel, qint64 volume); + +signals: + void nameChanged(); + void deviceChanged(); + void volumeChanged(); + void mutedChanged(); + void channelsChanged(); + void channelVolumesChanged(); + void deviceIndexChanged(); + +private: + void writeChanges(const pa_cvolume &volume, bool muted, const QString &device); + + QString m_name; + QString m_device; + pa_cvolume m_volume; + pa_channel_map m_channelMap; + QStringList m_channels; + bool m_muted; + + struct { + bool valid = false; + pa_cvolume volume; + bool muted; + QString device; + } m_cache; +}; + +} // QPulseAudio + +#endif // STREAMRESTORE_H diff --git a/plugins/systemvolume/streamrestore.cpp b/plugins/systemvolume/streamrestore.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/streamrestore.cpp @@ -0,0 +1,201 @@ +/* + Copyright 2016 David Rosca + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "streamrestore.h" +#include "context.h" +#include "debug.h" + +namespace QPulseAudio +{ + +StreamRestore::StreamRestore(quint32 index, const QVariantMap &properties, QObject *parent) + : PulseObject(parent) + , m_muted(false) +{ + memset(&m_volume, 0, sizeof(m_volume)); + memset(&m_channelMap, 0, sizeof(m_channelMap)); + + m_index = index; + m_properties = properties; +} + +void StreamRestore::update(const pa_ext_stream_restore_info *info) +{ + m_cache.valid = false; + const QString infoName = QString::fromUtf8(info->name); + if (m_name != infoName) { + m_name = infoName; + emit nameChanged(); + } + const QString infoDevice = QString::fromUtf8(info->device); + if (m_device != infoDevice) { + m_device = infoDevice; + emit deviceChanged(); + } + if (m_muted != info->mute) { + m_muted = info->mute; + emit mutedChanged(); + } + if (memcmp(&m_volume, &info->volume, sizeof(pa_cvolume)) != 0) { + m_volume = info->volume; + emit volumeChanged(); + emit channelVolumesChanged(); + } + if (memcmp(&m_channelMap, &info->channel_map, sizeof(pa_channel_map)) != 0) { + m_channels.clear(); + m_channels.reserve(info->channel_map.channels); + for (int i = 0; i < info->channel_map.channels; ++i) { + m_channels << QString::fromUtf8(pa_channel_position_to_pretty_string(info->channel_map.map[i])); + } + m_channelMap = info->channel_map; + emit channelsChanged(); + } +} + +QString StreamRestore::name() const +{ + return m_name; +} + +QString StreamRestore::device() const +{ + return m_device; +} + +void StreamRestore::setDevice(const QString &device) +{ + if (m_cache.valid) { + if (m_cache.device != device) { + writeChanges(m_cache.volume, m_cache.muted, device); + } + } else { + if (m_device != device) { + writeChanges(m_volume, m_muted, device); + } + } +} + +qint64 StreamRestore::volume() const +{ + return m_volume.values[0]; +} + +void StreamRestore::setVolume(qint64 volume) +{ + pa_cvolume vol = m_cache.valid ? m_cache.volume : m_volume; + vol.channels = 1; + vol.values[0] = volume; + + if (m_cache.valid) { + writeChanges(vol, m_cache.muted, m_cache.device); + } else { + writeChanges(vol, m_muted, m_device); + } +} + +bool StreamRestore::isMuted() const +{ + return m_muted; +} + +void StreamRestore::setMuted(bool muted) +{ + if (m_cache.valid) { + if (m_cache.muted != muted) { + writeChanges(m_cache.volume, muted, m_cache.device); + } + } else { + if (m_muted != muted) { + writeChanges(m_volume, muted, m_device); + } + } +} + +bool StreamRestore::hasVolume() const +{ + return true; +} + +bool StreamRestore::isVolumeWritable() const +{ + return true; +} + +QStringList StreamRestore::channels() const +{ + return m_channels; +} + +QList StreamRestore::channelVolumes() const +{ + QList ret; + ret.reserve(m_volume.channels); + for (int i = 0; i < m_volume.channels; ++i) { + ret << m_volume.values[i]; + } + return ret; +} + +void StreamRestore::setChannelVolume(int channel, qint64 volume) +{ + Q_ASSERT(channel >= 0 && channel < m_volume.channels); + pa_cvolume vol = m_cache.valid ? m_cache.volume : m_volume; + vol.values[channel] = volume; + + if (m_cache.valid) { + writeChanges(vol, m_cache.muted, m_cache.device); + } else { + writeChanges(vol, m_muted, m_device); + } +} + +quint32 StreamRestore::deviceIndex() const +{ + return PA_INVALID_INDEX; +} + +void StreamRestore::setDeviceIndex(quint32 deviceIndex) +{ + Q_UNUSED(deviceIndex); + qCWarning(PLASMAPA) << "Not implemented"; +} + +void StreamRestore::writeChanges(const pa_cvolume &volume, bool muted, const QString &device) +{ + const QByteArray nameData = m_name.toUtf8(); + const QByteArray deviceData = device.toUtf8(); + + pa_ext_stream_restore_info info; + info.name = nameData.constData(); + info.channel_map.channels = 1; + info.channel_map.map[0] = PA_CHANNEL_POSITION_MONO; + info.volume = volume; + info.device = deviceData.isEmpty() ? nullptr : deviceData.constData(); + info.mute = muted; + + m_cache.valid = true; + m_cache.volume = volume; + m_cache.muted = muted; + m_cache.device = device; + + context()->streamRestoreWrite(&info); +} + +} // QPulseAudio diff --git a/plugins/systemvolume/systemvolumeplugin.h b/plugins/systemvolume/systemvolumeplugin.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/systemvolumeplugin.h @@ -0,0 +1,51 @@ +/** + * Copyright 2017 Nicolas Fella + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SYSTEMVOLUMEPLUGIN_H +#define SYSTEMVOLUMEPLUGIN_H + +#include +#include + +#include + +#include "sink.h" + +#define PACKET_TYPE_SYSTEMVOLUME QStringLiteral("kdeconnect.systemvolume") + +class Q_DECL_EXPORT SystemvolumePlugin + : public KdeConnectPlugin +{ + Q_OBJECT + +public: + explicit SystemvolumePlugin(QObject* parent, const QVariantList& args); + ~SystemvolumePlugin() override; + + bool receivePacket(const NetworkPacket& np) override; + void connected() override; + +private: + void sendSinkList(); + void sendSinkUpdate(); + QMap sinksMap; +}; + +#endif diff --git a/plugins/systemvolume/systemvolumeplugin.cpp b/plugins/systemvolume/systemvolumeplugin.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/systemvolumeplugin.cpp @@ -0,0 +1,136 @@ +/** + * Copyright 2017 Nicolas Fella + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "systemvolumeplugin.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "sink.h" +#include "context.h" +#include "maps.h" + +K_PLUGIN_FACTORY_WITH_JSON( KdeConnectPluginFactory, "kdeconnect_systemvolume.json", registerPlugin< SystemvolumePlugin >(); ) + +Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_SYSTEMVOLUME, "kdeconnect.plugin.systemvolume") + +SystemvolumePlugin::SystemvolumePlugin(QObject* parent, const QVariantList& args) + : KdeConnectPlugin(parent, args) + , sinksMap() +{ + + connect(&QPulseAudio::Context::instance()->sinks(), &QPulseAudio::SinkMap::added, this, [this] { + sendSinkList(); + }); + + connect(&QPulseAudio::Context::instance()->sinks(), &QPulseAudio::SinkMap::removed, this, [this] { + sendSinkList(); + }); + +} + +SystemvolumePlugin::~SystemvolumePlugin() +{} + +bool SystemvolumePlugin::receivePacket(const NetworkPacket& np) +{ + if (np.type() == PACKET_TYPE_SYSTEMVOLUME) { + + if (QPulseAudio::Context::instance()->isValid()) { + + if (np.has(QStringLiteral("requestSinks"))) { + sendSinkList(); + } else if (np.has(QStringLiteral("volume"))) { + sinksMap[np.get(QStringLiteral("name"))]->setVolume(np.get(QStringLiteral("volume"))); + } else if (np.has(QStringLiteral("muted"))) { + qCDebug(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "set muted" << np.get(QStringLiteral("muted")); + sinksMap[np.get(QStringLiteral("name"))]->setMuted(np.get(QStringLiteral("muted"))); + } + + } + } + + return true; +} + +void SystemvolumePlugin::sendSinkList() { + + QJsonDocument document; + QJsonArray array; + + const QMap sinks = QPulseAudio::Context::instance()->sinks().data(); + QMap::const_iterator i; + + for (i = sinks.constBegin(); i != sinks.constEnd(); ++i) { + QPulseAudio::Sink* sink = i.value(); + sinksMap.insert(sink->name(), sink); + + connect(sink, &QPulseAudio::Sink::volumeChanged, this, [this, sink] { + NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME); + np.set(QStringLiteral("volume"), sink->volume()); + np.set(QStringLiteral("name"), sink->name()); + sendPacket(np); + }); + + connect(sink, &QPulseAudio::Sink::mutedChanged, this, [this, sink] { + NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME); + np.set(QStringLiteral("muted"), sink->isMuted()); + np.set(QStringLiteral("name"), sink->name()); + sendPacket(np); + }); + + QJsonObject sinkObject; + sinkObject.insert("name", sink->name()); + sinkObject.insert("description", sink->description()); + sinkObject.insert("muted", sink->isMuted()); + sinkObject.insert("volume", sink->volume()); + sinkObject.insert("maxVolume", QPulseAudio::Context::NormalVolume); + + array.append(sinkObject); + } + + document.setArray(array); + + NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME); + np.set(QStringLiteral("sinkList"), document); + sendPacket(np); +} + +void SystemvolumePlugin::connected() +{ + const QMap sinks = QPulseAudio::Context::instance()->sinks().data(); + QMap::const_iterator i; + + for (i = sinks.constBegin(); i != sinks.constEnd(); ++i) { + QPulseAudio::Sink* sink = i.value(); + sinksMap.insert(sink->name(), sink); + } +} + +#include "systemvolumeplugin.moc" + diff --git a/plugins/systemvolume/volumeobject.h b/plugins/systemvolume/volumeobject.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/volumeobject.h @@ -0,0 +1,101 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef VOLUMEOBJECT_H +#define VOLUMEOBJECT_H + +#include + +#include "pulseobject.h" + +namespace QPulseAudio +{ + +class VolumeObject : public PulseObject +{ + Q_OBJECT + Q_PROPERTY(qint64 volume READ volume WRITE setVolume NOTIFY volumeChanged) + Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged) + Q_PROPERTY(bool hasVolume READ hasVolume NOTIFY hasVolumeChanged) + Q_PROPERTY(bool volumeWritable READ isVolumeWritable NOTIFY isVolumeWritableChanged) + Q_PROPERTY(QStringList channels READ channels NOTIFY channelsChanged) + Q_PROPERTY(QList channelVolumes READ channelVolumes NOTIFY channelVolumesChanged) +public: + VolumeObject(QObject *parent); + virtual ~VolumeObject(); + + template + void updateVolumeObject(PAInfo *info) + { + updatePulseObject(info); + if (m_muted != info->mute) { + m_muted = info->mute; + emit mutedChanged(); + } + if (!pa_cvolume_equal(&m_volume, &info->volume)) { + m_volume = info->volume; + emit volumeChanged(); + emit channelVolumesChanged(); + } + QStringList infoChannels; + infoChannels.reserve(info->channel_map.channels); + for (int i = 0; i < info->channel_map.channels; ++i) { + infoChannels << QString::fromUtf8(pa_channel_position_to_pretty_string(info->channel_map.map[i])); + } + if (m_channels != infoChannels) { + m_channels = infoChannels; + emit channelsChanged(); + } + } + + qint64 volume() const; + virtual void setVolume(qint64 volume) = 0; + + bool isMuted() const; + virtual void setMuted(bool muted) = 0; + + bool hasVolume() const; + bool isVolumeWritable() const; + + QStringList channels() const; + QList channelVolumes() const; + Q_INVOKABLE virtual void setChannelVolume(int channel, qint64 volume) = 0; + +signals: + void volumeChanged(); + void mutedChanged(); + void hasVolumeChanged(); + void isVolumeWritableChanged(); + void channelsChanged(); + void channelVolumesChanged(); + +protected: + pa_cvolume cvolume() const; + + pa_cvolume m_volume; + bool m_muted; + bool m_hasVolume; + bool m_volumeWritable; + QStringList m_channels; +}; + +} // QPulseAudio + +#endif // VOLUMEOBJECT_H diff --git a/plugins/systemvolume/volumeobject.cpp b/plugins/systemvolume/volumeobject.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/volumeobject.cpp @@ -0,0 +1,79 @@ +/* + Copyright 2014-2015 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "volumeobject.h" + +namespace QPulseAudio +{ + +VolumeObject::VolumeObject(QObject *parent) + : PulseObject(parent) + , m_muted(true) + , m_hasVolume(true) + , m_volumeWritable(true) +{ + pa_cvolume_init(&m_volume); +} + +VolumeObject::~VolumeObject() +{ +} + +qint64 VolumeObject::volume() const +{ + return pa_cvolume_avg(&m_volume); +} + +bool VolumeObject::isMuted() const +{ + return m_muted; +} + +pa_cvolume VolumeObject::cvolume() const +{ + return m_volume; +} + +bool VolumeObject::hasVolume() const +{ + return m_hasVolume; +} + +bool VolumeObject::isVolumeWritable() const +{ + return m_volumeWritable; +} + +QStringList VolumeObject::channels() const +{ + return m_channels; +} + +QList VolumeObject::channelVolumes() const +{ + QList ret; + ret.reserve(m_volume.channels); + for (int i = 0; i < m_volume.channels; ++i) { + ret << m_volume.values[i]; + } + return ret; +} + +} // QPulseAudio