diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-05-12 21:32:53 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-05-12 21:32:53 -0400 |
| commit | f6f7c36909fa161efe53c40e9b4c34856e751536 (patch) | |
| tree | eff44527b0be61eb2e19c9f483ed38b72879af11 /src | |
Initial tmix skeleton — model layer + basic UI
PulseModel: stable PulseDevice objects keyed by PA index, updated
in-place via postEvent reconciliation. No bulk rebuilds. Three signals:
deviceAdded, deviceRemoved (device changed handled per-device via
volumeChanged/muteChanged/nameChanged).
MixerWindow: four-tab layout (Output/Input/Playback/Recording), adds
and removes individual DeviceWidgets in response to model signals.
Builds and links cleanly against TQt3/TDE + libpulse.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.cpp | 35 | ||||
| -rw-r--r-- | src/model/audiodevice.cpp | 3 | ||||
| -rw-r--r-- | src/model/audiodevice.h | 38 | ||||
| -rw-r--r-- | src/model/pulsedevice.cpp | 101 | ||||
| -rw-r--r-- | src/model/pulsedevice.h | 40 | ||||
| -rw-r--r-- | src/model/pulsemodel.cpp | 271 | ||||
| -rw-r--r-- | src/model/pulsemodel.h | 58 | ||||
| -rw-r--r-- | src/ui/devicewidget.cpp | 69 | ||||
| -rw-r--r-- | src/ui/devicewidget.h | 34 | ||||
| -rw-r--r-- | src/ui/mixerwindow.cpp | 85 | ||||
| -rw-r--r-- | src/ui/mixerwindow.h | 43 |
11 files changed, 777 insertions, 0 deletions
diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..6a8c6a8 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,35 @@ +#include <tdeapplication.h> +#include <tdecmdlineargs.h> +#include <tdeaboutdata.h> +#include <tdelocale.h> + +#include "model/pulsemodel.h" +#include "ui/mixerwindow.h" + +static TDECmdLineOptions options[] = { TDECmdLineLastOption }; + +int main( int argc, char **argv ) +{ + TDEAboutData about( + "tmix", I18N_NOOP("TMix"), + "0.1", + I18N_NOOP("Trinity audio mixer"), + TDEAboutData::License_GPL_V2, + "(C) 2024 Trinity Desktop Project" + ); + + TDECmdLineArgs::init( argc, argv, &about ); + TDECmdLineArgs::addCmdLineOptions( options ); + TDEApplication app; + + PulseModel model; + if ( !model.open() ) { + // TODO: show error dialog + return 1; + } + + MixerWindow win( &model ); + win.show(); + + return app.exec(); +} diff --git a/src/model/audiodevice.cpp b/src/model/audiodevice.cpp new file mode 100644 index 0000000..6a1debb --- /dev/null +++ b/src/model/audiodevice.cpp @@ -0,0 +1,3 @@ +#include "audiodevice.h" + +#include "audiodevice.moc" diff --git a/src/model/audiodevice.h b/src/model/audiodevice.h new file mode 100644 index 0000000..e753fc9 --- /dev/null +++ b/src/model/audiodevice.h @@ -0,0 +1,38 @@ +#pragma once + +#include <tqobject.h> +#include <tqstring.h> + +class TQWidget; + +class AudioDevice : public TQObject +{ + TQ_OBJECT + +public: + enum Category { + Output, // sinks, hardware output + Input, // sources, hardware input + Playback, // sink inputs (per-app playback streams) + Recording, // source outputs (per-app recording streams) + }; + + AudioDevice( TQObject *parent = 0 ) : TQObject(parent) {} + virtual ~AudioDevice() {} + + virtual TQString name() const = 0; + virtual Category category() const = 0; + + virtual int volume() const = 0; // 0-100 + virtual bool muted() const = 0; + virtual void setVolume( int v ) = 0; + virtual void setMuted( bool m ) = 0; + + // Creates the widget for this device. Caller takes ownership. + virtual TQWidget *createWidget( TQWidget *parent ) = 0; + +signals: + void volumeChanged( int v ); + void muteChanged( bool m ); + void nameChanged( const TQString &name ); +}; diff --git a/src/model/pulsedevice.cpp b/src/model/pulsedevice.cpp new file mode 100644 index 0000000..e0f84ab --- /dev/null +++ b/src/model/pulsedevice.cpp @@ -0,0 +1,101 @@ +#include "pulsedevice.h" +#include "../ui/devicewidget.h" +#include <tqwidget.h> + +PulseDevice::PulseDevice( AudioDevice::Category cat, uint32_t paIndex, TQObject *parent ) + : AudioDevice(parent), + m_category(cat), m_paIndex(paIndex), + m_volume(0), m_muted(false), + m_context(0), m_mainloop(0) +{ +} + +void PulseDevice::setPAContext( pa_context *ctx, pa_threaded_mainloop *mainloop ) +{ + m_context = ctx; + m_mainloop = mainloop; +} + +void PulseDevice::update( const TQString &name, int volume, bool muted ) +{ + bool nameChange = ( name != m_name ); + bool volumeChange = ( volume != m_volume ); + bool muteChange = ( muted != m_muted ); + + m_name = name; + m_volume = volume; + m_muted = muted; + + if ( nameChange ) emit nameChanged( m_name ); + if ( volumeChange ) emit volumeChanged( m_volume ); + if ( muteChange ) emit muteChanged( m_muted ); +} + +// ---- write back to PA ------------------------------------------------------- + +static pa_volume_t percentToPA( int pct ) +{ + if ( pct <= 0 ) return 0; + if ( pct >= 100 ) return PA_VOLUME_NORM; + return (pa_volume_t)( (double)pct / 100.0 * PA_VOLUME_NORM + 0.5 ); +} + +void PulseDevice::setVolume( int v ) +{ + if ( !m_context ) return; + v = v < 0 ? 0 : v > 100 ? 100 : v; + + pa_cvolume cv; + pa_cvolume_set( &cv, 2, percentToPA(v) ); + + pa_threaded_mainloop_lock( m_mainloop ); + pa_operation *op = 0; + switch ( m_category ) { + case Output: + op = pa_context_set_sink_volume_by_index( m_context, m_paIndex, &cv, 0, 0 ); + break; + case Input: + op = pa_context_set_source_volume_by_index( m_context, m_paIndex, &cv, 0, 0 ); + break; + case Playback: + op = pa_context_set_sink_input_volume( m_context, m_paIndex, &cv, 0, 0 ); + break; + case Recording: + op = pa_context_set_source_output_volume( m_context, m_paIndex, &cv, 0, 0 ); + break; + } + if ( op ) pa_operation_unref( op ); + pa_threaded_mainloop_unlock( m_mainloop ); +} + +void PulseDevice::setMuted( bool m ) +{ + if ( !m_context ) return; + + pa_threaded_mainloop_lock( m_mainloop ); + pa_operation *op = 0; + int mute = m ? 1 : 0; + switch ( m_category ) { + case Output: + op = pa_context_set_sink_mute_by_index( m_context, m_paIndex, mute, 0, 0 ); + break; + case Input: + op = pa_context_set_source_mute_by_index( m_context, m_paIndex, mute, 0, 0 ); + break; + case Playback: + op = pa_context_set_sink_input_mute( m_context, m_paIndex, mute, 0, 0 ); + break; + case Recording: + op = pa_context_set_source_output_mute( m_context, m_paIndex, mute, 0, 0 ); + break; + } + if ( op ) pa_operation_unref( op ); + pa_threaded_mainloop_unlock( m_mainloop ); +} + +TQWidget *PulseDevice::createWidget( TQWidget *parent ) +{ + return new DeviceWidget( this, parent ); +} + +#include "pulsedevice.moc" diff --git a/src/model/pulsedevice.h b/src/model/pulsedevice.h new file mode 100644 index 0000000..fbdf985 --- /dev/null +++ b/src/model/pulsedevice.h @@ -0,0 +1,40 @@ +#pragma once + +#include <pulse/pulseaudio.h> +#include "audiodevice.h" + +class PulseDevice : public AudioDevice +{ + TQ_OBJECT + +public: + PulseDevice( AudioDevice::Category cat, uint32_t paIndex, TQObject *parent = 0 ); + ~PulseDevice() {} + + TQString name() const { return m_name; } + Category category() const { return m_category; } + int volume() const { return m_volume; } + bool muted() const { return m_muted; } + uint32_t paIndex() const { return m_paIndex; } + + void setVolume( int v ); + void setMuted( bool m ); + + // Called by PulseModel when PA reports an update — updates in place and emits signals. + void update( const TQString &name, int volume, bool muted ); + + TQWidget *createWidget( TQWidget *parent ); + + // PA context needed to write volume back — set once by PulseModel after construction. + void setPAContext( pa_context *ctx, pa_threaded_mainloop *mainloop ); + +private: + AudioDevice::Category m_category; + uint32_t m_paIndex; + TQString m_name; + int m_volume; // 0-100 + bool m_muted; + + pa_context *m_context; + pa_threaded_mainloop *m_mainloop; +}; diff --git a/src/model/pulsemodel.cpp b/src/model/pulsemodel.cpp new file mode 100644 index 0000000..de745bc --- /dev/null +++ b/src/model/pulsemodel.cpp @@ -0,0 +1,271 @@ +#include "pulsemodel.h" +#include "pulsedevice.h" + +#include <tqapplication.h> + +// Custom event posted from the PA thread to the main thread. +static const int PA_EVENT = TQEvent::User + 1; + +struct PAEvent : public TQCustomEvent { + enum Kind { DeviceAdded, DeviceRemoved, DeviceUpdated }; + Kind kind; + AudioDevice::Category cat; + uint32_t paIndex; + TQString name; + int volume; + bool muted; + + PAEvent( Kind k, AudioDevice::Category c, uint32_t idx, + const TQString &n = TQString(), int vol = 0, bool m = false ) + : TQCustomEvent(PA_EVENT), kind(k), cat(c), paIndex(idx), + name(n), volume(vol), muted(m) {} +}; + +// ---- helpers ---------------------------------------------------------------- + +static int paVolumeToPercent( const pa_cvolume &cv ) +{ + pa_volume_t avg = pa_cvolume_avg( &cv ); + return (int)( (double)avg / PA_VOLUME_NORM * 100.0 + 0.5 ); +} + +// ---- ctor / dtor ------------------------------------------------------------ + +PulseModel::PulseModel( TQObject *parent ) + : TQObject(parent), m_mainloop(0), m_context(0) +{ + m_sinks.setAutoDelete( true ); + m_sources.setAutoDelete( true ); + m_sinkInputs.setAutoDelete( true ); + m_sourceOutputs.setAutoDelete( true ); +} + +PulseModel::~PulseModel() +{ + close(); +} + +// ---- connect / disconnect --------------------------------------------------- + +bool PulseModel::open() +{ + m_mainloop = pa_threaded_mainloop_new(); + if ( !m_mainloop ) return false; + + pa_mainloop_api *api = pa_threaded_mainloop_get_api( m_mainloop ); + m_context = pa_context_new( api, "tmix" ); + if ( !m_context ) { + pa_threaded_mainloop_free( m_mainloop ); + m_mainloop = 0; + return false; + } + + pa_context_set_state_callback( m_context, contextStateCb, this ); + pa_threaded_mainloop_lock( m_mainloop ); + pa_threaded_mainloop_start( m_mainloop ); + pa_context_connect( m_context, 0, PA_CONTEXT_NOFLAGS, 0 ); + pa_threaded_mainloop_unlock( m_mainloop ); + return true; +} + +void PulseModel::close() +{ + if ( m_context ) { + pa_threaded_mainloop_lock( m_mainloop ); + pa_context_disconnect( m_context ); + pa_context_unref( m_context ); + m_context = 0; + pa_threaded_mainloop_unlock( m_mainloop ); + } + if ( m_mainloop ) { + pa_threaded_mainloop_stop( m_mainloop ); + pa_threaded_mainloop_free( m_mainloop ); + m_mainloop = 0; + } + m_sinks.clear(); + m_sources.clear(); + m_sinkInputs.clear(); + m_sourceOutputs.clear(); +} + +// ---- public query ----------------------------------------------------------- + +TQPtrList<AudioDevice> PulseModel::devices( AudioDevice::Category cat ) const +{ + TQPtrList<AudioDevice> result; + const TQPtrList<PulseDevice> *src = 0; + switch ( cat ) { + case AudioDevice::Output: src = &m_sinks; break; + case AudioDevice::Input: src = &m_sources; break; + case AudioDevice::Playback: src = &m_sinkInputs; break; + case AudioDevice::Recording: src = &m_sourceOutputs; break; + } + if ( src ) + for ( TQPtrListIterator<PulseDevice> it(*src); *it; ++it ) + result.append( *it ); + return result; +} + +// ---- PA thread callbacks (called from PA thread — only post events) --------- + +void PulseModel::contextStateCb( pa_context *c, void *userdata ) +{ + PulseModel *self = static_cast<PulseModel *>(userdata); + switch ( pa_context_get_state(c) ) { + case PA_CONTEXT_READY: + pa_context_set_subscribe_callback( c, subscribeCb, self ); + pa_context_subscribe( c, + (pa_subscription_mask_t)( + PA_SUBSCRIPTION_MASK_SINK | + PA_SUBSCRIPTION_MASK_SOURCE | + PA_SUBSCRIPTION_MASK_SINK_INPUT | + PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT ), + 0, 0 ); + self->enumerateAll(); + break; + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + break; + default: + break; + } +} + +void PulseModel::enumerateAll() +{ + // Called from PA thread (inside context state callback at READY). + pa_operation_unref( pa_context_get_sink_info_list( m_context, sinkInfoCb, this ) ); + pa_operation_unref( pa_context_get_source_info_list( m_context, sourceInfoCb, this ) ); + pa_operation_unref( pa_context_get_sink_input_info_list( m_context, sinkInputInfoCb, this ) ); + pa_operation_unref( pa_context_get_source_output_info_list( m_context, sourceOutputInfoCb, this ) ); +} + +void PulseModel::sinkInfoCb( pa_context *, const pa_sink_info *info, int eol, void *userdata ) +{ + if ( eol || !info ) return; + PulseModel *self = static_cast<PulseModel *>(userdata); + TQApplication::postEvent( self, new PAEvent( + PAEvent::DeviceAdded, AudioDevice::Output, info->index, + TQString::fromUtf8( info->description ), + paVolumeToPercent( info->volume ), info->mute != 0 ) ); +} + +void PulseModel::sourceInfoCb( pa_context *, const pa_source_info *info, int eol, void *userdata ) +{ + if ( eol || !info ) return; + // Skip monitor sources — they're PA internals, not real inputs. + if ( info->monitor_of_sink != PA_INVALID_INDEX ) return; + PulseModel *self = static_cast<PulseModel *>(userdata); + TQApplication::postEvent( self, new PAEvent( + PAEvent::DeviceAdded, AudioDevice::Input, info->index, + TQString::fromUtf8( info->description ), + paVolumeToPercent( info->volume ), info->mute != 0 ) ); +} + +void PulseModel::sinkInputInfoCb( pa_context *, const pa_sink_input_info *info, int eol, void *userdata ) +{ + if ( eol || !info ) return; + PulseModel *self = static_cast<PulseModel *>(userdata); + TQApplication::postEvent( self, new PAEvent( + PAEvent::DeviceAdded, AudioDevice::Playback, info->index, + TQString::fromUtf8( info->name ), + paVolumeToPercent( info->volume ), info->mute != 0 ) ); +} + +void PulseModel::sourceOutputInfoCb( pa_context *, const pa_source_output_info *info, int eol, void *userdata ) +{ + if ( eol || !info ) return; + PulseModel *self = static_cast<PulseModel *>(userdata); + TQApplication::postEvent( self, new PAEvent( + PAEvent::DeviceAdded, AudioDevice::Recording, info->index, + TQString::fromUtf8( info->name ), + paVolumeToPercent( info->volume ), info->mute != 0 ) ); +} + +void PulseModel::subscribeCb( pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata ) +{ + PulseModel *self = static_cast<PulseModel *>(userdata); + + pa_subscription_event_type_t facility = (pa_subscription_event_type_t)( t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK ); + pa_subscription_event_type_t evtype = (pa_subscription_event_type_t)( t & PA_SUBSCRIPTION_EVENT_TYPE_MASK ); + + AudioDevice::Category cat; + switch ( facility ) { + case PA_SUBSCRIPTION_EVENT_SINK: cat = AudioDevice::Output; break; + case PA_SUBSCRIPTION_EVENT_SOURCE: cat = AudioDevice::Input; break; + case PA_SUBSCRIPTION_EVENT_SINK_INPUT: cat = AudioDevice::Playback; break; + case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: cat = AudioDevice::Recording; break; + default: return; + } + + if ( evtype == PA_SUBSCRIPTION_EVENT_REMOVE ) { + TQApplication::postEvent( self, new PAEvent( PAEvent::DeviceRemoved, cat, idx ) ); + return; + } + + // NEW or CHANGE — re-query to get current state. + pa_operation *op = 0; + switch ( facility ) { + case PA_SUBSCRIPTION_EVENT_SINK: + op = pa_context_get_sink_info_by_index( c, idx, sinkInfoCb, self ); + break; + case PA_SUBSCRIPTION_EVENT_SOURCE: + op = pa_context_get_source_info_by_index( c, idx, sourceInfoCb, self ); + break; + case PA_SUBSCRIPTION_EVENT_SINK_INPUT: + op = pa_context_get_sink_input_info( c, idx, sinkInputInfoCb, self ); + break; + case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: + op = pa_context_get_source_output_info( c, idx, sourceOutputInfoCb, self ); + break; + default: break; + } + if ( op ) pa_operation_unref( op ); +} + +// ---- main thread event handler ---------------------------------------------- + +void PulseModel::customEvent( TQCustomEvent *e ) +{ + if ( e->type() != PA_EVENT ) return; + PAEvent *ev = static_cast<PAEvent *>(e); + + TQPtrList<PulseDevice> *list = 0; + switch ( ev->cat ) { + case AudioDevice::Output: list = &m_sinks; break; + case AudioDevice::Input: list = &m_sources; break; + case AudioDevice::Playback: list = &m_sinkInputs; break; + case AudioDevice::Recording: list = &m_sourceOutputs; break; + } + + if ( ev->kind == PAEvent::DeviceRemoved ) { + PulseDevice *dev = findDevice( *list, ev->paIndex ); + if ( dev ) { + emit deviceRemoved( dev ); + list->remove( dev ); // autoDelete=true, so dev is deleted here + } + return; + } + + // DeviceAdded or DeviceUpdated (both come through the same info callbacks). + PulseDevice *dev = findDevice( *list, ev->paIndex ); + if ( dev ) { + dev->update( ev->name, ev->volume, ev->muted ); + } else { + dev = new PulseDevice( ev->cat, ev->paIndex, this ); + dev->setPAContext( m_context, m_mainloop ); + dev->update( ev->name, ev->volume, ev->muted ); + list->append( dev ); + emit deviceAdded( dev ); + } +} + +PulseDevice *PulseModel::findDevice( TQPtrList<PulseDevice> &list, uint32_t paIndex ) +{ + for ( TQPtrListIterator<PulseDevice> it(list); *it; ++it ) + if ( (*it)->paIndex() == paIndex ) + return *it; + return 0; +} + +#include "pulsemodel.moc" diff --git a/src/model/pulsemodel.h b/src/model/pulsemodel.h new file mode 100644 index 0000000..b96f2b1 --- /dev/null +++ b/src/model/pulsemodel.h @@ -0,0 +1,58 @@ +#pragma once + +#include <tqobject.h> +#include <tqptrlist.h> + +#include <pulse/pulseaudio.h> + +#include "audiodevice.h" + +class PulseDevice; + +class PulseModel : public TQObject +{ + TQ_OBJECT + +public: + explicit PulseModel( TQObject *parent = 0 ); + ~PulseModel(); + + bool open(); + void close(); + + TQPtrList<AudioDevice> devices( AudioDevice::Category cat ) const; + +protected: + void customEvent( TQCustomEvent *e ); + +signals: + void deviceAdded( AudioDevice *dev ); + void deviceRemoved( AudioDevice *dev ); + void ready(); // emitted once initial enumeration is complete + +private: + static void contextStateCb( pa_context *c, void *userdata ); + static void sinkInfoCb( pa_context *c, const pa_sink_info *info, int eol, void *userdata ); + static void sourceInfoCb( pa_context *c, const pa_source_info *info, int eol, void *userdata ); + static void sinkInputInfoCb( pa_context *c, const pa_sink_input_info *info, int eol, void *userdata ); + static void sourceOutputInfoCb( pa_context *c, const pa_source_output_info *info, int eol, void *userdata ); + static void subscribeCb( pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata ); + + void enumerateAll(); + void addOrUpdateSink( const pa_sink_info *info ); + void addOrUpdateSource( const pa_source_info *info ); + void addOrUpdateSinkInput( const pa_sink_input_info *info ); + void addOrUpdateSourceOutput( const pa_source_output_info *info ); + void removeDevice( AudioDevice::Category cat, uint32_t paIndex ); + + pa_threaded_mainloop *m_mainloop; + pa_context *m_context; + + // Keyed by PA index within each category — stable objects, updated in place. + TQPtrList<PulseDevice> m_sinks; + TQPtrList<PulseDevice> m_sources; + TQPtrList<PulseDevice> m_sinkInputs; + TQPtrList<PulseDevice> m_sourceOutputs; + + PulseDevice *findDevice( TQPtrList<PulseDevice> &list, uint32_t paIndex ); +}; diff --git a/src/ui/devicewidget.cpp b/src/ui/devicewidget.cpp new file mode 100644 index 0000000..420d5fd --- /dev/null +++ b/src/ui/devicewidget.cpp @@ -0,0 +1,69 @@ +#include "devicewidget.h" +#include "../model/audiodevice.h" + +#include <tqlabel.h> +#include <tqlayout.h> +#include <tqslider.h> +#include <tqtoolbutton.h> + +DeviceWidget::DeviceWidget( AudioDevice *device, TQWidget *parent ) + : TQWidget(parent), m_device(device) +{ + TQVBoxLayout *layout = new TQVBoxLayout( this, 4, 2 ); + + m_label = new TQLabel( device->name(), this ); + m_label->setAlignment( TQLabel::AlignHCenter ); + + m_slider = new TQSlider( 0, 100, 5, device->volume(), TQt::Vertical, this ); + m_slider->setTickmarks( TQSlider::NoMarks ); + + m_muteButton = new TQToolButton( this ); + m_muteButton->setToggleButton( true ); + m_muteButton->setTextLabel( "M" ); + m_muteButton->setOn( device->muted() ); + + layout->addWidget( m_label ); + layout->addWidget( m_slider, 1 ); + layout->addWidget( m_muteButton, 0, TQt::AlignHCenter ); + + // slider → device + connect( m_slider, TQ_SIGNAL(valueChanged(int)), this, TQ_SLOT(onVolumeChanged(int)) ); + connect( m_muteButton, TQ_SIGNAL(clicked()), this, TQ_SLOT(onMuteToggled()) ); + + // device → widget + connect( device, TQ_SIGNAL(volumeChanged(int)), this, TQ_SLOT(onDeviceVolume(int)) ); + connect( device, TQ_SIGNAL(muteChanged(bool)), this, TQ_SLOT(onDeviceMute(bool)) ); + connect( device, TQ_SIGNAL(nameChanged(const TQString&)), this, TQ_SLOT(onDeviceName(const TQString&)) ); +} + +void DeviceWidget::onVolumeChanged( int v ) +{ + m_device->setVolume( v ); +} + +void DeviceWidget::onMuteToggled() +{ + m_device->setMuted( !m_device->muted() ); +} + +void DeviceWidget::onDeviceVolume( int v ) +{ + // Block signal to avoid feedback loop back to device. + m_slider->blockSignals( true ); + m_slider->setValue( v ); + m_slider->blockSignals( false ); +} + +void DeviceWidget::onDeviceMute( bool m ) +{ + m_muteButton->blockSignals( true ); + m_muteButton->setOn( m ); + m_muteButton->blockSignals( false ); +} + +void DeviceWidget::onDeviceName( const TQString &name ) +{ + m_label->setText( name ); +} + +#include "devicewidget.moc" diff --git a/src/ui/devicewidget.h b/src/ui/devicewidget.h new file mode 100644 index 0000000..4a6cead --- /dev/null +++ b/src/ui/devicewidget.h @@ -0,0 +1,34 @@ +#pragma once + +#include <tqwidget.h> +#include <tqstring.h> + +class AudioDevice; +class TQSlider; +class TQToolButton; +class TQLabel; +class TQVBoxLayout; + +class DeviceWidget : public TQWidget +{ + TQ_OBJECT + +public: + DeviceWidget( AudioDevice *device, TQWidget *parent = 0 ); + ~DeviceWidget() {} + + AudioDevice *device() const { return m_device; } + +private slots: + void onVolumeChanged( int v ); // slider → device + void onMuteToggled(); // button → device + void onDeviceVolume( int v ); // device → slider + void onDeviceMute( bool m ); // device → button + void onDeviceName( const TQString &name ); + +private: + AudioDevice *m_device; + TQSlider *m_slider; + TQToolButton *m_muteButton; + TQLabel *m_label; +}; diff --git a/src/ui/mixerwindow.cpp b/src/ui/mixerwindow.cpp new file mode 100644 index 0000000..91cea65 --- /dev/null +++ b/src/ui/mixerwindow.cpp @@ -0,0 +1,85 @@ +#include "mixerwindow.h" +#include "../model/pulsemodel.h" + +#include <tqlayout.h> +#include <tqscrollview.h> +#include <ktabwidget.h> +#include <tdelocale.h> + +static MixerWindow::Tab makeTab( KTabWidget *tabs, const TQString &label ) +{ + MixerWindow::Tab t; + TQScrollView *scroll = new TQScrollView( tabs ); + scroll->setResizePolicy( TQScrollView::AutoOneFit ); + scroll->setHScrollBarMode( TQScrollView::Auto ); + scroll->setVScrollBarMode( TQScrollView::AlwaysOff ); + scroll->setFrameStyle( TQFrame::NoFrame ); + + t.page = new TQWidget( scroll->viewport() ); + scroll->addChild( t.page ); + t.layout = new TQHBoxLayout( t.page, 6, 4 ); + + tabs->addTab( scroll, label ); + return t; +} + +MixerWindow::MixerWindow( PulseModel *model, TQWidget *parent ) + : TQWidget(parent), m_model(model) +{ + setCaption( i18n("TMix") ); + + TQVBoxLayout *top = new TQVBoxLayout( this, 4, 0 ); + m_tabs = new KTabWidget( this ); + top->addWidget( m_tabs ); + + m_output = makeTab( m_tabs, i18n("Output") ); + m_input = makeTab( m_tabs, i18n("Input") ); + m_playback = makeTab( m_tabs, i18n("Playback") ); + m_recording = makeTab( m_tabs, i18n("Recording") ); + + connect( model, TQ_SIGNAL(deviceAdded(AudioDevice*)), this, TQ_SLOT(onDeviceAdded(AudioDevice*)) ); + connect( model, TQ_SIGNAL(deviceRemoved(AudioDevice*)), this, TQ_SLOT(onDeviceRemoved(AudioDevice*)) ); + + // Populate with any devices already known at construction time. + AudioDevice::Category cats[] = { + AudioDevice::Output, AudioDevice::Input, + AudioDevice::Playback, AudioDevice::Recording + }; + for ( int i = 0; i < 4; i++ ) { + TQPtrList<AudioDevice> devs = model->devices( cats[i] ); + for ( TQPtrListIterator<AudioDevice> it(devs); *it; ++it ) + onDeviceAdded( *it ); + } +} + +MixerWindow::Tab &MixerWindow::tabForCategory( AudioDevice::Category cat ) +{ + switch ( cat ) { + case AudioDevice::Output: return m_output; + case AudioDevice::Input: return m_input; + case AudioDevice::Playback: return m_playback; + case AudioDevice::Recording: return m_recording; + } + return m_output; +} + +void MixerWindow::onDeviceAdded( AudioDevice *dev ) +{ + Tab &t = tabForCategory( dev->category() ); + TQWidget *w = dev->createWidget( t.page ); + t.layout->addWidget( w ); + w->show(); + m_widgets.insert( dev, w ); +} + +void MixerWindow::onDeviceRemoved( AudioDevice *dev ) +{ + TQWidget *w = m_widgets[dev]; + if ( !w ) return; + Tab &t = tabForCategory( dev->category() ); + t.layout->remove( w ); + m_widgets.remove( dev ); + delete w; +} + +#include "mixerwindow.moc" diff --git a/src/ui/mixerwindow.h b/src/ui/mixerwindow.h new file mode 100644 index 0000000..f6b7be8 --- /dev/null +++ b/src/ui/mixerwindow.h @@ -0,0 +1,43 @@ +#pragma once + +#include <tqwidget.h> +#include <tqmap.h> + +#include "../model/audiodevice.h" + +class PulseModel; +class KTabWidget; +class TQHBoxLayout; + +class MixerWindow : public TQWidget +{ + TQ_OBJECT + +public: + explicit MixerWindow( PulseModel *model, TQWidget *parent = 0 ); + ~MixerWindow() {} + +private slots: + void onDeviceAdded( AudioDevice *dev ); + void onDeviceRemoved( AudioDevice *dev ); + +public: + struct Tab { + TQWidget *page; + TQHBoxLayout *layout; + }; + +private: + + Tab &tabForCategory( AudioDevice::Category cat ); + + PulseModel *m_model; + KTabWidget *m_tabs; + + Tab m_output; + Tab m_input; + Tab m_playback; + Tab m_recording; + + TQMap<AudioDevice*, TQWidget*> m_widgets; +}; |
