commit 02648e86c39777bb2979dc2df81256987fbb294f Author: mappu Date: Mon Oct 1 20:00:27 2018 +1300 initial commit diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..287b2e6 --- /dev/null +++ b/main.cpp @@ -0,0 +1,13 @@ +#include +#include "trayicon.h" + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + a.setApplicationName(QStringLiteral("Set audio card profile...")); + + auto ic = new TrayIcon(&a); + ic->show(); + + return a.exec(); +} diff --git a/pa-card-profile-tray.pro b/pa-card-profile-tray.pro new file mode 100644 index 0000000..9dfc63f --- /dev/null +++ b/pa-card-profile-tray.pro @@ -0,0 +1,19 @@ +QT += core gui widgets + +TARGET = pa-card-profile-tray +TEMPLATE = app + +PKGCONFIG += libpulse +LIBS += -lpulse + +DEFINES += QT_DEPRECATED_WARNINGS + +SOURCES += main.cpp \ + trayicon.cpp \ + qpulse.cpp \ + threadedmainlooplock.cpp + +HEADERS += \ + trayicon.h \ + qpulse.h \ + threadedmainlooplock.h diff --git a/pa-card-profile-tray.pro.user b/pa-card-profile-tray.pro.user new file mode 100644 index 0000000..1129cae --- /dev/null +++ b/pa-card-profile-tray.pro.user @@ -0,0 +1,336 @@ + + + + + + EnvironmentId + {67050bf3-78d3-448a-82be-9e893e07d9a7} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + true + 0 + 8 + true + 1 + true + true + true + false + + + + ProjectExplorer.Project.PluginSettings + + + + ProjectExplorer.Project.Target.0 + + Debian System Qt5 + Debian System Qt5 + {4cec843c-77aa-4b21-bd99-b7bf84d54e1f} + 0 + 0 + 0 + + /home/mjg/dev/.build/build-pa-card-profile-tray-Debian_System_Qt5-Debug + + + true + qmake + + QtProjectManager.QMakeBuildStep + true + + false + false + false + + + true + Make + + Qt4ProjectManager.MakeStep + + -w + -r + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + -w + -r + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Debug + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + true + + + /home/mjg/dev/.build/build-pa-card-profile-tray-Debian_System_Qt5-Release + + + true + qmake + + QtProjectManager.QMakeBuildStep + false + + false + false + true + + + true + Make + + Qt4ProjectManager.MakeStep + + -w + -r + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + -w + -r + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + true + + + /home/mjg/dev/.build/build-pa-card-profile-tray-Debian_System_Qt5-Profile + + + true + qmake + + QtProjectManager.QMakeBuildStep + true + + false + true + true + + + true + Make + + Qt4ProjectManager.MakeStep + + -w + -r + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + -w + -r + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Profile + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + true + + 3 + + + 0 + Deploy + + ProjectExplorer.BuildSteps.Deploy + + 1 + Deploy Configuration + + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + pa-card-profile-tray + + Qt4ProjectManager.Qt4RunConfiguration:/home/mjg/dev/pa-card-profile-tray/pa-card-profile-tray.pro + true + + pa-card-profile-tray.pro + false + + + 3768 + false + true + false + false + true + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 18 + + + Version + 18 + + diff --git a/qpulse.cpp b/qpulse.cpp new file mode 100644 index 0000000..1198dd2 --- /dev/null +++ b/qpulse.cpp @@ -0,0 +1,66 @@ +#include "qpulse.h" +#include "threadedmainlooplock.h" + +QPulse::QPulse(QObject *parent) : + QObject(parent), + ml(pa_threaded_mainloop_new()), + c(nullptr) +{ + pa_threaded_mainloop_start(ml); + + c = pa_context_new( pa_threaded_mainloop_get_api(ml), "pa-card-profile-tray"); + pa_context_set_state_callback(c, &QPulse::onConnectionStateChangedSt, this); + pa_context_connect(c, nullptr, PA_CONTEXT_NOFLAGS, nullptr); +} + +QPulse::~QPulse() +{ + pa_context_disconnect(c); + pa_threaded_mainloop_stop(ml); + pa_threaded_mainloop_free(ml); +} + +pa_context_state_t QPulse::ConnectionState() +{ + //ThreadedMainLoopLock lock(ml); + + return pa_context_get_state(c); +} + +void QPulse::RequestServerInfo() +{ + //ThreadedMainLoopLock lock(ml); + + pa_context_get_server_info(c, &QPulse::onGotServerInfoSt, this); +} + +void QPulse::RequestCardInfo() +{ + //ThreadedMainLoopLock lock(ml); + + pa_context_get_card_info_list(c, &QPulse::onGotCardInfoListSt, this); +} + +void QPulse::onGotCardInfoListSt(pa_context*, const pa_card_info *i, int eol, void *userdata) +{ + QPulse* qp = static_cast(userdata); + //ThreadedMainLoopLock lock(qp->ml); + + emit qp->GotCardInfoList(i, eol); +} + +void QPulse::onGotServerInfoSt(pa_context*, const pa_server_info *i, void *userdata) +{ + QPulse* qp = static_cast(userdata); + //ThreadedMainLoopLock lock(qp->ml); + + emit qp->GotServerInfo(i); +} + +void QPulse::onConnectionStateChangedSt(pa_context*, void *userdata) +{ + QPulse* qp = static_cast(userdata); + //ThreadedMainLoopLock lock(qp->ml); + + emit qp->ConnectionStateChanged(); +} diff --git a/qpulse.h b/qpulse.h new file mode 100644 index 0000000..7cbfbee --- /dev/null +++ b/qpulse.h @@ -0,0 +1,37 @@ +#ifndef QPULSE_H +#define QPULSE_H + +#include +#include + +class QPulse : public QObject +{ + Q_OBJECT +public: + explicit QPulse(QObject *parent = 0); + ~QPulse(); + + pa_context_state_t ConnectionState(); + + void RequestServerInfo(); + void RequestCardInfo(); + +signals: + void ConnectionStateChanged(); + void GotServerInfo(const pa_server_info *i); + void GotCardInfoList(const pa_card_info *i, int eol); + +public slots: + +protected: + pa_threaded_mainloop* ml; + pa_context *c; + +private: + static void onConnectionStateChangedSt(pa_context *c, void *userdata); + static void onGotServerInfoSt(pa_context *c, const pa_server_info *i, void *userdata); + static void onGotCardInfoListSt(pa_context *c, const pa_card_info *i, int eol, void *userdata); + +}; + +#endif // QPULSE_H diff --git a/threadedmainlooplock.cpp b/threadedmainlooplock.cpp new file mode 100644 index 0000000..d797190 --- /dev/null +++ b/threadedmainlooplock.cpp @@ -0,0 +1,11 @@ +#include "threadedmainlooplock.h" + +ThreadedMainLoopLock::ThreadedMainLoopLock(pa_threaded_mainloop* ml) : + ml(ml) +{ + pa_threaded_mainloop_lock(ml); +} + +ThreadedMainLoopLock::~ThreadedMainLoopLock() { + pa_threaded_mainloop_unlock(ml); +} diff --git a/threadedmainlooplock.h b/threadedmainlooplock.h new file mode 100644 index 0000000..61145eb --- /dev/null +++ b/threadedmainlooplock.h @@ -0,0 +1,19 @@ +#ifndef THREADEDMAINLOOPLOCK_H +#define THREADEDMAINLOOPLOCK_H + +#include +#include + +class ThreadedMainLoopLock { +public: + ThreadedMainLoopLock(pa_threaded_mainloop *ml); + ~ThreadedMainLoopLock(); + +private: + Q_DISABLE_COPY(ThreadedMainLoopLock) + +private: + pa_threaded_mainloop* ml; +}; + +#endif // THREADEDMAINLOOPLOCK_H diff --git a/trayicon.cpp b/trayicon.cpp new file mode 100644 index 0000000..9f40d9a --- /dev/null +++ b/trayicon.cpp @@ -0,0 +1,110 @@ +#include "trayicon.h" + +#include +#include +#include +#include + +#include + +TrayIcon::TrayIcon(QObject *parent) : + QSystemTrayIcon(QIcon::fromTheme("preferences-desktop-sound"), parent), + pulse(this) +{ + this->menu = new QMenu(); // can only be explicitly owned by a QWidget, not any QObject, so we need to + // manually manage its lifecycle in the d'tor + + this->quitAction = new QAction("Quit"); + connect(this->quitAction, &QAction::triggered, this, &TrayIcon::on_quitAction_triggered); + + this->refreshAction = new QAction("Refresh"); + connect(this->refreshAction, &QAction::triggered, this, &TrayIcon::onRefreshAction_triggered); + + // Set up basic menu only, will be wiped during refresh + this->menu->addAction(refreshAction); + this->menu->addAction(quitAction); + + this->setContextMenu(this->menu); // n.b. does not take ownership + + connect(&this->pulse, &QPulse::ConnectionStateChanged, [=]() { + if (pulse.ConnectionState() == PA_CONTEXT_READY) { + refreshData(); + } + }); + connect(&this->pulse, &QPulse::GotCardInfoList, this, &TrayIcon::onGotPulseCardInfo); + + refreshData(); +} + +TrayIcon::~TrayIcon() +{ + for (int i = 0; i < cardMenus.length(); ++i) { + delete cardMenus[i]; + } + cardMenus.clear(); + delete this->quitAction; + delete this->menu; +} + +void TrayIcon::refreshData() +{ + // Delete existing card menus, but leave the final clearing for later + for (int i = 0; i < cardMenus.length(); ++i) { + delete cardMenus[i]; + } + cardMenus.clear(); + + // Calling delete() should have removed them from the menu, leaving only the separator / refresh / quit options + + pulse.RequestCardInfo(); +} + +void TrayIcon::on_quitAction_triggered() +{ + QApplication::exit(0); +} + +void TrayIcon::onRefreshAction_triggered() +{ + refreshData(); +} + +void TrayIcon::onGotPulseCardInfo(const pa_card_info *cardInfo, int eol) +{ + if (eol) { + // OK - rebuild the menu + this->menu->clear(); + + for (int i = 0; i < this->cardMenus.length(); ++i) { + this->menu->addMenu(this->cardMenus[i]); + } + this->menu->addSeparator(); + this->menu->addAction(refreshAction); + this->menu->addAction(quitAction); + return; + } + + /* + QString name; // = pa_proplist_to_string(cardInfo->proplist);; // copy to QString from internally-managed buffers + if (true) { // 0 != pa_proplist_isempty(cardInfo->proplist) && 1 == pa_proplist_contains(cardInfo->proplist, PA_PROP_DEVICE_DESCRIPTION)) { + name = pa_proplist_gets(cardInfo->proplist, PA_PROP_DEVICE_DESCRIPTION);; + } else { + name = cardInfo->name; + } + */ + + /* + QString name(cardInfo->name); + if (name.length() == 0) { + name = tr("Default card"); + } + */ + + QMenu* cardMenu = new QMenu(name); + for (size_t i = 0; i < cardInfo->n_profiles; ++i) { + auto profileAction = new QAction(cardInfo->profiles[i].description, cardMenu); + cardMenu->addAction(profileAction); + } + + this->cardMenus.push_back(cardMenu); +} diff --git a/trayicon.h b/trayicon.h new file mode 100644 index 0000000..b083fc3 --- /dev/null +++ b/trayicon.h @@ -0,0 +1,32 @@ +#ifndef TRAYICON_H +#define TRAYICON_H + +#include +#include +#include +#include +#include +#include "qpulse.h" + +class TrayIcon : public QSystemTrayIcon +{ +public: + TrayIcon(QObject *parent); + ~TrayIcon(); + + void refreshData(); + +public slots: + void on_quitAction_triggered(); + void onRefreshAction_triggered(); + void onGotPulseCardInfo(const pa_card_info *i, int eol); + +protected: + QPulse pulse; + QMenu* menu; + QVector cardMenus; + QAction* quitAction; + QAction* refreshAction; +}; + +#endif // TRAYICON_H