summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.cpp28
-rw-r--r--src/model/audiodevice.h7
-rw-r--r--src/model/pulsedevice.cpp227
-rw-r--r--src/model/pulsedevice.h42
-rw-r--r--src/model/pulsemodel.cpp451
-rw-r--r--src/model/pulsemodel.h66
-rw-r--r--src/ui/balanceknob.cpp124
-rw-r--r--src/ui/balanceknob.h34
-rw-r--r--src/ui/devicespage.cpp251
-rw-r--r--src/ui/devicespage.h42
-rw-r--r--src/ui/devicewidget.cpp384
-rw-r--r--src/ui/devicewidget.h55
-rw-r--r--src/ui/kledbutton.cpp29
-rw-r--r--src/ui/kledbutton.h24
-rw-r--r--src/ui/levelmeter.cpp67
-rw-r--r--src/ui/levelmeter.h17
-rw-r--r--src/ui/mixerwindow.cpp381
-rw-r--r--src/ui/mixerwindow.h51
-rw-r--r--src/ui/preferencesdlg.cpp111
-rw-r--r--src/ui/preferencesdlg.h37
-rw-r--r--src/ui/tmixapp.cpp20
-rw-r--r--src/ui/tmixapp.h17
-rw-r--r--src/ui/tmixpopup.cpp134
-rw-r--r--src/ui/tmixpopup.h42
-rw-r--r--src/ui/tmixtray.cpp182
-rw-r--r--src/ui/tmixtray.h36
26 files changed, 2712 insertions, 147 deletions
diff --git a/src/main.cpp b/src/main.cpp
index 6a8c6a8..e8e7d36 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,10 +1,13 @@
-#include <tdeapplication.h>
+#include <kuniqueapplication.h>
#include <tdecmdlineargs.h>
#include <tdeaboutdata.h>
#include <tdelocale.h>
+#include <tdeconfig.h>
+#include <tdeglobal.h>
#include "model/pulsemodel.h"
#include "ui/mixerwindow.h"
+#include "ui/tmixapp.h"
static TDECmdLineOptions options[] = { TDECmdLineLastOption };
@@ -20,16 +23,27 @@ int main( int argc, char **argv )
TDECmdLineArgs::init( argc, argv, &about );
TDECmdLineArgs::addCmdLineOptions( options );
- TDEApplication app;
+ KUniqueApplication::addCmdLineOptions();
- PulseModel model;
- if ( !model.open() ) {
- // TODO: show error dialog
+ if ( !KUniqueApplication::start() )
+ return 0;
+
+ TmixApp app;
+ app.disableSessionManagement();
+
+ PulseModel *model = new PulseModel;
+ if ( !model->open() ) {
+ delete model;
return 1;
}
- MixerWindow win( &model );
- win.show();
+ MixerWindow *win = new MixerWindow( model );
+ app.setMainWindow( win );
+
+ TDEConfig *cfg = TDEGlobal::config();
+ cfg->setGroup("General");
+ if ( !cfg->readBoolEntry("DockInTray", true) )
+ win->show();
return app.exec();
}
diff --git a/src/model/audiodevice.h b/src/model/audiodevice.h
index e753fc9..854ba35 100644
--- a/src/model/audiodevice.h
+++ b/src/model/audiodevice.h
@@ -21,12 +21,16 @@ public:
virtual ~AudioDevice() {}
virtual TQString name() const = 0;
+ virtual TQString iconName() const = 0;
virtual Category category() const = 0;
virtual int volume() const = 0; // 0-100
virtual bool muted() const = 0;
+ virtual int pan() const = 0; // -50 (left) .. 0 .. +50 (right)
+
virtual void setVolume( int v ) = 0;
virtual void setMuted( bool m ) = 0;
+ virtual void setPan( int p ) = 0;
// Creates the widget for this device. Caller takes ownership.
virtual TQWidget *createWidget( TQWidget *parent ) = 0;
@@ -34,5 +38,8 @@ public:
signals:
void volumeChanged( int v );
void muteChanged( bool m );
+ void panChanged( int p );
+ void levelChanged( float level );
void nameChanged( const TQString &name );
+ void recordingActiveChanged( bool active );
};
diff --git a/src/model/pulsedevice.cpp b/src/model/pulsedevice.cpp
index e0f84ab..de778b9 100644
--- a/src/model/pulsedevice.cpp
+++ b/src/model/pulsedevice.cpp
@@ -1,37 +1,168 @@
#include "pulsedevice.h"
+#include "pulsemodel.h"
#include "../ui/devicewidget.h"
-#include <tqwidget.h>
+
+#include <tqapplication.h>
+#include <string.h>
+
+// ---- peak-level event (PA thread → main thread) -----------------------------
+
+static const int PA_LEVEL_EVENT = TQEvent::User + 2;
+
+struct PALevelEvent : public TQCustomEvent {
+ float level;
+ PALevelEvent( float l ) : TQCustomEvent(PA_LEVEL_EVENT), level(l) {}
+};
+
+// ---- ctor / dtor ------------------------------------------------------------
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)
+ m_category(cat), m_paIndex(paIndex), m_sinkIndex(PA_INVALID_INDEX),
+ m_volume(0), m_muted(false), m_pan(0), m_channels(2),
+ m_context(0), m_mainloop(0),
+ m_monitorStream(0), m_model(0), m_recordingCount(0)
+{
+}
+
+PulseDevice::~PulseDevice()
{
+ // detach() should have been called by PulseModel before destruction;
+ // this is a safety net for cases where it wasn't.
+ if ( m_monitorStream ) {
+ pa_stream_set_read_callback( m_monitorStream, 0, 0 );
+ pa_stream_unref( m_monitorStream );
+ m_monitorStream = 0;
+ }
}
+// ---- context wiring ---------------------------------------------------------
+
void PulseDevice::setPAContext( pa_context *ctx, pa_threaded_mainloop *mainloop )
{
m_context = ctx;
m_mainloop = mainloop;
+ startMonitoring();
+}
+
+void PulseDevice::detach()
+{
+ if ( m_monitorStream && m_mainloop ) {
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_stream_set_read_callback( m_monitorStream, 0, 0 );
+ pa_stream_disconnect( m_monitorStream );
+ pa_stream_unref( m_monitorStream );
+ m_monitorStream = 0;
+ pa_threaded_mainloop_unlock( m_mainloop );
+ }
+ m_context = 0;
+ m_mainloop = 0;
}
-void PulseDevice::update( const TQString &name, int volume, bool muted )
+// ---- update -----------------------------------------------------------------
+
+void PulseDevice::update( const TQString &name, int volume, bool muted,
+ uint8_t channels, int pan,
+ const TQString &monitorName, const TQString &iconName )
{
bool nameChange = ( name != m_name );
bool volumeChange = ( volume != m_volume );
bool muteChange = ( muted != m_muted );
+ bool panChange = ( pan != m_pan );
+
+ m_name = name;
+ m_iconName = iconName;
+ m_volume = volume;
+ m_muted = muted;
+ m_channels = channels ? channels : 2;
+ m_pan = pan;
- m_name = name;
- m_volume = volume;
- m_muted = muted;
+ if ( m_monitorName.isEmpty() && !monitorName.isEmpty() ) {
+ m_monitorName = monitorName;
+ if ( m_context ) startMonitoring();
+ }
if ( nameChange ) emit nameChanged( m_name );
if ( volumeChange ) emit volumeChanged( m_volume );
if ( muteChange ) emit muteChanged( m_muted );
+ if ( panChange ) emit panChanged( m_pan );
}
-// ---- write back to PA -------------------------------------------------------
+// ---- peak monitoring --------------------------------------------------------
+
+void PulseDevice::startMonitoring()
+{
+ if ( !m_context || !m_mainloop || m_monitorName.isEmpty() || m_monitorStream )
+ return;
+
+ pa_sample_spec spec;
+ spec.format = PA_SAMPLE_FLOAT32NE;
+ spec.rate = 25;
+ spec.channels = 1;
+
+ pa_threaded_mainloop_lock( m_mainloop );
+
+ pa_proplist *props = pa_proplist_new();
+ pa_proplist_sets( props, PA_PROP_MEDIA_ROLE, "abstract" );
+ m_monitorStream = pa_stream_new_with_proplist( m_context, "tmix-peak", &spec, 0, props );
+ pa_proplist_free( props );
+ if ( m_monitorStream ) {
+ pa_stream_set_read_callback( m_monitorStream, monitorReadCb, this );
+
+ // For sink inputs, filter peak detection to just this stream.
+ if ( m_category == Playback )
+ pa_stream_set_monitor_stream( m_monitorStream, m_paIndex );
+
+ pa_buffer_attr attr;
+ memset( &attr, 0xff, sizeof(attr) );
+ attr.fragsize = sizeof(float);
+
+ pa_stream_connect_record( m_monitorStream,
+ m_monitorName.utf8().data(), &attr,
+ (pa_stream_flags_t)(PA_STREAM_PEAK_DETECT |
+ PA_STREAM_ADJUST_LATENCY |
+ PA_STREAM_DONT_MOVE) );
+ }
+
+ pa_threaded_mainloop_unlock( m_mainloop );
+}
+
+void PulseDevice::monitorReadCb( pa_stream *s, size_t /*nbytes*/, void *userdata )
+{
+ PulseDevice *self = static_cast<PulseDevice*>(userdata);
+ const void *data = 0;
+ size_t len = 0;
+
+ if ( pa_stream_peek( s, &data, &len ) < 0 ) return;
+
+ float level = 0.0f;
+ if ( data && len >= sizeof(float) )
+ level = *static_cast<const float*>(data);
+ if ( level < 0.0f ) level = 0.0f;
+ if ( level > 1.0f ) level = 1.0f;
+
+ pa_stream_drop( s );
+
+ TQApplication::postEvent( self, new PALevelEvent(level) );
+}
+
+void PulseDevice::customEvent( TQCustomEvent *e )
+{
+ if ( e->type() == PA_LEVEL_EVENT )
+ emit levelChanged( static_cast<PALevelEvent*>(e)->level );
+}
+
+void PulseDevice::adjustRecordingCount( int delta )
+{
+ bool wasBefore = ( m_recordingCount > 0 );
+ m_recordingCount += delta;
+ if ( m_recordingCount < 0 ) m_recordingCount = 0;
+ bool isNow = ( m_recordingCount > 0 );
+ if ( isNow != wasBefore )
+ emit recordingActiveChanged( isNow );
+}
+
+// ---- helpers ----------------------------------------------------------------
static pa_volume_t percentToPA( int pct )
{
@@ -40,29 +171,60 @@ static pa_volume_t percentToPA( int pct )
return (pa_volume_t)( (double)pct / 100.0 * PA_VOLUME_NORM + 0.5 );
}
+static pa_cvolume buildCVolume( uint8_t channels, pa_volume_t vol, int pan )
+{
+ pa_cvolume cv;
+ cv.channels = channels;
+ if ( channels >= 2 ) {
+ double lf = ( pan >= 0 ) ? ( 50.0 - pan ) / 50.0 : 1.0;
+ double rf = ( pan <= 0 ) ? ( 50.0 + pan ) / 50.0 : 1.0;
+ cv.values[0] = (pa_volume_t)( vol * lf );
+ cv.values[1] = (pa_volume_t)( vol * rf );
+ for ( uint8_t i = 2; i < channels; i++ )
+ cv.values[i] = vol;
+ } else {
+ pa_cvolume_set( &cv, channels, vol );
+ }
+ return cv;
+}
+
+// ---- write back to PA -------------------------------------------------------
+
void PulseDevice::setVolume( int v )
{
if ( !m_context ) return;
v = v < 0 ? 0 : v > 100 ? 100 : v;
+ m_volume = v;
+ emit volumeChanged( v );
- pa_cvolume cv;
- pa_cvolume_set( &cv, 2, percentToPA(v) );
+ pa_cvolume cv = buildCVolume( m_channels, percentToPA(v), m_pan );
+ 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::setPan( int p )
+{
+ if ( !m_context ) return;
+ p = p < -50 ? -50 : p > 50 ? 50 : p;
+ m_pan = p;
+ emit panChanged( p );
+
+ pa_cvolume cv = buildCVolume( m_channels, percentToPA(m_volume), p );
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;
+ 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 );
@@ -71,23 +233,14 @@ void PulseDevice::setVolume( int v )
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;
+ 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 );
@@ -95,7 +248,7 @@ void PulseDevice::setMuted( bool m )
TQWidget *PulseDevice::createWidget( TQWidget *parent )
{
- return new DeviceWidget( this, parent );
+ return new DeviceWidget( this, m_model, parent );
}
#include "pulsedevice.moc"
diff --git a/src/model/pulsedevice.h b/src/model/pulsedevice.h
index fbdf985..e974508 100644
--- a/src/model/pulsedevice.h
+++ b/src/model/pulsedevice.h
@@ -3,38 +3,68 @@
#include <pulse/pulseaudio.h>
#include "audiodevice.h"
+class PulseModel;
+
class PulseDevice : public AudioDevice
{
TQ_OBJECT
public:
PulseDevice( AudioDevice::Category cat, uint32_t paIndex, TQObject *parent = 0 );
- ~PulseDevice() {}
+ ~PulseDevice();
TQString name() const { return m_name; }
+ TQString iconName() const { return m_iconName; }
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; }
+ int pan() const { return m_pan; }
+ uint32_t paIndex() const { return m_paIndex; }
+ uint32_t sinkIndex() const { return m_sinkIndex; }
+ TQString monitorName() const { return m_monitorName; }
+ TQString paName() const { return m_paName; }
+
+ void setPaName( const TQString &n ) { m_paName = n; }
+ void setSinkIndex( uint32_t idx ) { m_sinkIndex = idx; }
+ void setModel( PulseModel *m ) { m_model = m; }
+ void adjustRecordingCount( int delta );
void setVolume( int v );
void setMuted( bool m );
+ void setPan( int p );
- // Called by PulseModel when PA reports an update — updates in place and emits signals.
- void update( const TQString &name, int volume, bool muted );
+ void update( const TQString &name, int volume, bool muted,
+ uint8_t channels, int pan,
+ const TQString &monitorName, const TQString &iconName );
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 );
+ void detach(); // called by PulseModel before tearing down the mainloop
+
+protected:
+ void customEvent( TQCustomEvent *e );
private:
+ void startMonitoring();
+ static void monitorReadCb( pa_stream *s, size_t nbytes, void *userdata );
+
AudioDevice::Category m_category;
uint32_t m_paIndex;
+ uint32_t m_sinkIndex;
TQString m_name;
- int m_volume; // 0-100
+ TQString m_iconName;
+ int m_volume;
bool m_muted;
+ int m_pan;
+ uint8_t m_channels;
pa_context *m_context;
pa_threaded_mainloop *m_mainloop;
+
+ TQString m_monitorName;
+ pa_stream *m_monitorStream;
+ TQString m_paName;
+ PulseModel *m_model;
+ int m_recordingCount;
};
diff --git a/src/model/pulsemodel.cpp b/src/model/pulsemodel.cpp
index de745bc..a677edc 100644
--- a/src/model/pulsemodel.cpp
+++ b/src/model/pulsemodel.cpp
@@ -3,8 +3,17 @@
#include <tqapplication.h>
-// Custom event posted from the PA thread to the main thread.
-static const int PA_EVENT = TQEvent::User + 1;
+// Custom events posted from the PA thread to the main thread.
+static const int PA_EVENT = TQEvent::User + 1;
+static const int PA_SERVER_EVENT = TQEvent::User + 3;
+static const int PA_CARD_EVENT = TQEvent::User + 4;
+
+struct PAServerEvent : public TQCustomEvent {
+ TQString defaultSinkName;
+ TQString defaultSourceName;
+ PAServerEvent( const TQString &sink, const TQString &source )
+ : TQCustomEvent(PA_SERVER_EVENT), defaultSinkName(sink), defaultSourceName(source) {}
+};
struct PAEvent : public TQCustomEvent {
enum Kind { DeviceAdded, DeviceRemoved, DeviceUpdated };
@@ -14,19 +23,123 @@ struct PAEvent : public TQCustomEvent {
TQString name;
int volume;
bool muted;
+ uint8_t channels;
+ int pan;
+ TQString monitorName;
+ TQString iconName;
+ TQString paName; // raw PA name (sinks/sources only, for default tracking)
+ uint32_t parentIndex; // for Playback: parent sink PA index
PAEvent( Kind k, AudioDevice::Category c, uint32_t idx,
- const TQString &n = TQString(), int vol = 0, bool m = false )
+ const TQString &n = TQString(), int vol = 0, bool m = false,
+ uint8_t ch = 2, int p = 0, const TQString &mon = TQString(),
+ const TQString &icon = TQString(), uint32_t parent = PA_INVALID_INDEX,
+ const TQString &pname = TQString() )
: TQCustomEvent(PA_EVENT), kind(k), cat(c), paIndex(idx),
- name(n), volume(vol), muted(m) {}
+ name(n), volume(vol), muted(m), channels(ch), pan(p),
+ monitorName(mon), iconName(icon), paName(pname), parentIndex(parent) {}
+};
+
+struct PACardEvent : public TQCustomEvent {
+ struct Profile { TQString name; TQString description; bool available; };
+ struct Port {
+ TQString name; TQString description;
+ int available; int direction; uint32_t type; TQString availGroup;
+ };
+ bool removed;
+ uint32_t index;
+ TQString name;
+ TQString description;
+ TQString activeProfile;
+ TQString vendor;
+ TQString product;
+ TQString formFactor;
+ TQString busType;
+ TQValueList<Profile> profiles;
+ TQValueList<Port> ports;
+
+ explicit PACardEvent( uint32_t idx )
+ : TQCustomEvent(PA_CARD_EVENT), removed(true), index(idx) {}
+ PACardEvent( uint32_t idx, const TQString &n, const TQString &d, const TQString &ap )
+ : TQCustomEvent(PA_CARD_EVENT), removed(false), index(idx),
+ name(n), description(d), activeProfile(ap) {}
};
// ---- helpers ----------------------------------------------------------------
+static bool isGenericMediaName( const TQString &s )
+{
+ TQString l = s.lower().stripWhiteSpace();
+ return l == "audio stream" || l == "alsa playback" || l == "playback"
+ || l == "audio output" || l == "output" || l.isEmpty();
+}
+
+static TQString paCleanAppName( const char *raw )
+{
+ if ( !raw || !*raw ) return TQString();
+ TQString s = TQString::fromUtf8( raw );
+
+ // Strip "ALSA plug-in [Foo]" wrapper → "Foo"
+ int lb = s.find('['), rb = s.findRev(']');
+ if ( lb != -1 && rb > lb )
+ s = s.mid( lb + 1, rb - lb - 1 ).stripWhiteSpace();
+
+ // Strip trailing "app" (amarokapp → amarok), then capitalise first letter
+ if ( s.endsWith("app") && s.length() > 3 )
+ s = s.left( s.length() - 3 );
+ if ( !s.isEmpty() )
+ s[0] = s[0].upper();
+
+ return s;
+}
+
+static TQString paStreamName( pa_proplist *pl, const char *fallback )
+{
+ TQString appName = paCleanAppName( pa_proplist_gets(pl, PA_PROP_APPLICATION_NAME) );
+ const char *mn = pa_proplist_gets(pl, PA_PROP_MEDIA_NAME);
+ TQString mediaName = mn ? TQString::fromUtf8(mn).stripWhiteSpace() : TQString();
+
+ if ( !appName.isEmpty() && !isGenericMediaName(mediaName) && !mediaName.isEmpty() )
+ return appName + " - " + mediaName;
+ if ( !appName.isEmpty() )
+ return appName;
+ if ( !isGenericMediaName(mediaName) && !mediaName.isEmpty() )
+ return mediaName;
+ return fallback ? TQString::fromUtf8(fallback) : TQString();
+}
+
+static TQString paAppIcon( pa_proplist *pl )
+{
+ const char *s;
+ if ( (s = pa_proplist_gets(pl, PA_PROP_APPLICATION_ICON_NAME)) && *s )
+ return TQString::fromUtf8(s);
+ if ( (s = pa_proplist_gets(pl, "application.process.binary")) && *s ) {
+ TQString bin = TQString::fromUtf8(s).lower();
+ // Some TDE apps register binary as "fooapp" but icon as "foo"
+ if ( bin.endsWith("app") )
+ return bin.left( bin.length() - 3 );
+ return bin;
+ }
+ if ( (s = pa_proplist_gets(pl, PA_PROP_APPLICATION_NAME)) && *s )
+ return TQString::fromUtf8(s).lower();
+ return TQString();
+}
+
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 );
+ pa_volume_t mx = pa_cvolume_max( &cv );
+ return (int)( (double)mx / PA_VOLUME_NORM * 100.0 + 0.5 );
+}
+
+// Derive pan -50..+50 from the first two channels of a cvolume.
+static int cvolumeToPan( const pa_cvolume &cv )
+{
+ if ( cv.channels < 2 ) return 0;
+ double l = cv.values[0];
+ double r = cv.values[1];
+ double mx = ( l > r ) ? l : r;
+ if ( mx < 1.0 ) return 0;
+ return (int)( ( r - l ) / mx * 50.0 );
}
// ---- ctor / dtor ------------------------------------------------------------
@@ -70,6 +183,12 @@ bool PulseModel::open()
void PulseModel::close()
{
+ // Detach monitor streams on all devices before tearing down the mainloop.
+ TQPtrList<PulseDevice> *allLists[4] = { &m_sinks, &m_sources, &m_sinkInputs, &m_sourceOutputs };
+ for ( int i = 0; i < 4; i++ )
+ for ( TQPtrListIterator<PulseDevice> it(*allLists[i]); *it; ++it )
+ (*it)->detach();
+
if ( m_context ) {
pa_threaded_mainloop_lock( m_mainloop );
pa_context_disconnect( m_context );
@@ -119,8 +238,11 @@ void PulseModel::contextStateCb( pa_context *c, void *userdata )
PA_SUBSCRIPTION_MASK_SINK |
PA_SUBSCRIPTION_MASK_SOURCE |
PA_SUBSCRIPTION_MASK_SINK_INPUT |
- PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT ),
+ PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT |
+ PA_SUBSCRIPTION_MASK_SERVER |
+ PA_SUBSCRIPTION_MASK_CARD ),
0, 0 );
+ pa_operation_unref( pa_context_get_server_info( c, serverInfoCb, self ) );
self->enumerateAll();
break;
case PA_CONTEXT_FAILED:
@@ -138,16 +260,32 @@ void PulseModel::enumerateAll()
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 ) );
+ pa_operation_unref( pa_context_get_card_info_list( m_context, cardInfoCb, this ) );
+}
+
+void PulseModel::serverInfoCb( pa_context *, const pa_server_info *info, void *userdata )
+{
+ if ( !info ) return;
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+ TQApplication::postEvent( self, new PAServerEvent(
+ TQString::fromUtf8( info->default_sink_name ? info->default_sink_name : "" ),
+ TQString::fromUtf8( info->default_source_name ? info->default_source_name : "" ) ) );
}
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 *ev = new PAEvent(
PAEvent::DeviceAdded, AudioDevice::Output, info->index,
TQString::fromUtf8( info->description ),
- paVolumeToPercent( info->volume ), info->mute != 0 ) );
+ paVolumeToPercent( info->volume ), info->mute != 0,
+ info->volume.channels, cvolumeToPan( info->volume ),
+ TQString::fromUtf8( info->monitor_source_name ),
+ TQString::fromUtf8( pa_proplist_gets( info->proplist, PA_PROP_DEVICE_ICON_NAME ) ? : "" ),
+ PA_INVALID_INDEX,
+ TQString::fromUtf8( info->name ) );
+ TQApplication::postEvent( self, ev );
}
void PulseModel::sourceInfoCb( pa_context *, const pa_source_info *info, int eol, void *userdata )
@@ -159,7 +297,12 @@ void PulseModel::sourceInfoCb( pa_context *, const pa_source_info *info, int eol
TQApplication::postEvent( self, new PAEvent(
PAEvent::DeviceAdded, AudioDevice::Input, info->index,
TQString::fromUtf8( info->description ),
- paVolumeToPercent( info->volume ), info->mute != 0 ) );
+ paVolumeToPercent( info->volume ), info->mute != 0,
+ info->volume.channels, cvolumeToPan( info->volume ),
+ TQString::fromUtf8( info->name ),
+ TQString::fromUtf8( pa_proplist_gets( info->proplist, PA_PROP_DEVICE_ICON_NAME ) ? : "" ),
+ PA_INVALID_INDEX,
+ TQString::fromUtf8( info->name ) ) );
}
void PulseModel::sinkInputInfoCb( pa_context *, const pa_sink_input_info *info, int eol, void *userdata )
@@ -168,18 +311,27 @@ void PulseModel::sinkInputInfoCb( pa_context *, const pa_sink_input_info *info,
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 ) );
+ paStreamName( info->proplist, info->name ),
+ paVolumeToPercent( info->volume ), info->mute != 0,
+ info->volume.channels, cvolumeToPan( info->volume ),
+ TQString(), // monitor name filled in by customEvent from parent sink
+ paAppIcon( info->proplist ),
+ info->sink ) ); // parent sink index for peak monitoring
}
void PulseModel::sourceOutputInfoCb( pa_context *, const pa_source_output_info *info, int eol, void *userdata )
{
if ( eol || !info ) return;
+ // Filter out our own peak-monitor streams.
+ if ( info->name && strcmp(info->name, "tmix-peak") == 0 ) 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 ) );
+ paStreamName( info->proplist, info->name ),
+ paVolumeToPercent( info->volume ), info->mute != 0,
+ info->volume.channels, cvolumeToPan( info->volume ),
+ TQString(), paAppIcon( info->proplist ),
+ info->source ) ); // parentIndex = source being recorded from
}
void PulseModel::subscribeCb( pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata )
@@ -189,6 +341,21 @@ void PulseModel::subscribeCb( pa_context *c, pa_subscription_event_type_t t, uin
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 );
+ if ( facility == PA_SUBSCRIPTION_EVENT_SERVER ) {
+ pa_operation_unref( pa_context_get_server_info( c, serverInfoCb, self ) );
+ return;
+ }
+
+ if ( facility == PA_SUBSCRIPTION_EVENT_CARD ) {
+ if ( evtype == PA_SUBSCRIPTION_EVENT_REMOVE ) {
+ TQApplication::postEvent( self, new PACardEvent( idx ) );
+ } else {
+ pa_operation *op = pa_context_get_card_info_by_index( c, idx, cardInfoCb, self );
+ if ( op ) pa_operation_unref( op );
+ }
+ return;
+ }
+
AudioDevice::Category cat;
switch ( facility ) {
case PA_SUBSCRIPTION_EVENT_SINK: cat = AudioDevice::Output; break;
@@ -223,10 +390,134 @@ void PulseModel::subscribeCb( pa_context *c, pa_subscription_event_type_t t, uin
if ( op ) pa_operation_unref( op );
}
+void PulseModel::cardInfoCb( pa_context *, const pa_card_info *info, int eol, void *userdata )
+{
+ if ( eol || !info ) return;
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+
+ const char *desc = pa_proplist_gets( info->proplist, PA_PROP_DEVICE_DESCRIPTION );
+ const char *vendor = pa_proplist_gets( info->proplist, PA_PROP_DEVICE_VENDOR_NAME );
+ const char *product = pa_proplist_gets( info->proplist, PA_PROP_DEVICE_PRODUCT_NAME );
+ const char *ff = pa_proplist_gets( info->proplist, PA_PROP_DEVICE_FORM_FACTOR );
+ const char *bus = pa_proplist_gets( info->proplist, "device.bus" );
+
+ PACardEvent *ev = new PACardEvent(
+ info->index,
+ TQString::fromUtf8( info->name ),
+ TQString::fromUtf8( desc ? desc : info->name ),
+ info->active_profile2 ? TQString::fromUtf8( info->active_profile2->name ) : TQString() );
+
+ ev->vendor = TQString::fromUtf8( vendor ? vendor : "" );
+ ev->product = TQString::fromUtf8( product ? product : "" );
+ ev->formFactor = TQString::fromUtf8( ff ? ff : "" );
+ ev->busType = TQString::fromUtf8( bus ? bus : "" );
+
+ for ( uint32_t i = 0; info->profiles2 && info->profiles2[i]; ++i ) {
+ PACardEvent::Profile p;
+ p.name = TQString::fromUtf8( info->profiles2[i]->name );
+ p.description = TQString::fromUtf8( info->profiles2[i]->description );
+ p.available = info->profiles2[i]->available != 0;
+ ev->profiles.append( p );
+ }
+
+ for ( uint32_t i = 0; i < info->n_ports; ++i ) {
+ PACardEvent::Port port;
+ port.name = TQString::fromUtf8( info->ports[i]->name );
+ port.description= TQString::fromUtf8( info->ports[i]->description );
+ port.available = info->ports[i]->available;
+ port.direction = info->ports[i]->direction;
+ port.type = info->ports[i]->type;
+ if ( info->ports[i]->availability_group )
+ port.availGroup = TQString::fromUtf8( info->ports[i]->availability_group );
+ ev->ports.append( port );
+ }
+
+ TQApplication::postEvent( self, ev );
+}
+
// ---- main thread event handler ----------------------------------------------
void PulseModel::customEvent( TQCustomEvent *e )
{
+ if ( e->type() == PA_CARD_EVENT ) {
+ PACardEvent *ev = static_cast<PACardEvent *>(e);
+ if ( ev->removed ) {
+ for ( TQValueList<PulseCardInfo>::Iterator it = m_cards.begin();
+ it != m_cards.end(); ++it ) {
+ if ( it->index == ev->index ) {
+ m_cards.remove( it );
+ emit cardRemoved( ev->index );
+ return;
+ }
+ }
+ return;
+ }
+ PulseCardInfo *existing = findCard( ev->index );
+ if ( existing ) {
+ existing->name = ev->name;
+ existing->description = ev->description;
+ existing->activeProfile = ev->activeProfile;
+ existing->vendor = ev->vendor;
+ existing->product = ev->product;
+ existing->formFactor = ev->formFactor;
+ existing->busType = ev->busType;
+ existing->profiles.clear();
+ existing->ports.clear();
+ for ( TQValueList<PACardEvent::Profile>::Iterator it = ev->profiles.begin();
+ it != ev->profiles.end(); ++it ) {
+ PulseCardProfile p; p.name = it->name; p.description = it->description; p.available = it->available;
+ existing->profiles.append( p );
+ }
+ for ( TQValueList<PACardEvent::Port>::Iterator it = ev->ports.begin();
+ it != ev->ports.end(); ++it ) {
+ PulseCardPort port;
+ port.name = it->name; port.description = it->description;
+ port.available = it->available; port.direction = it->direction;
+ port.type = it->type; port.availabilityGroup = it->availGroup;
+ existing->ports.append( port );
+ }
+ emit cardUpdated( ev->index );
+ } else {
+ PulseCardInfo info;
+ info.index = ev->index;
+ info.name = ev->name;
+ info.description = ev->description;
+ info.activeProfile = ev->activeProfile;
+ info.vendor = ev->vendor;
+ info.product = ev->product;
+ info.formFactor = ev->formFactor;
+ info.busType = ev->busType;
+ for ( TQValueList<PACardEvent::Profile>::Iterator it = ev->profiles.begin();
+ it != ev->profiles.end(); ++it ) {
+ PulseCardProfile p; p.name = it->name; p.description = it->description; p.available = it->available;
+ info.profiles.append( p );
+ }
+ for ( TQValueList<PACardEvent::Port>::Iterator it = ev->ports.begin();
+ it != ev->ports.end(); ++it ) {
+ PulseCardPort port;
+ port.name = it->name; port.description = it->description;
+ port.available = it->available; port.direction = it->direction;
+ port.type = it->type; port.availabilityGroup = it->availGroup;
+ info.ports.append( port );
+ }
+ m_cards.append( info );
+ emit cardAdded( ev->index );
+ }
+ return;
+ }
+
+ if ( e->type() == PA_SERVER_EVENT ) {
+ PAServerEvent *ev = static_cast<PAServerEvent *>(e);
+ if ( ev->defaultSinkName != m_defaultSinkName ) {
+ m_defaultSinkName = ev->defaultSinkName;
+ emit defaultOutputChanged( defaultOutput() );
+ }
+ if ( ev->defaultSourceName != m_defaultSourceName ) {
+ m_defaultSourceName = ev->defaultSourceName;
+ emit defaultInputChanged( defaultInput() );
+ }
+ return;
+ }
if ( e->type() != PA_EVENT ) return;
PAEvent *ev = static_cast<PAEvent *>(e);
@@ -241,25 +532,89 @@ void PulseModel::customEvent( TQCustomEvent *e )
if ( ev->kind == PAEvent::DeviceRemoved ) {
PulseDevice *dev = findDevice( *list, ev->paIndex );
if ( dev ) {
+ // Remove from name maps if present.
+ TQMap<TQString,PulseDevice*>::Iterator it;
+ for ( it = m_sinksByName.begin(); it != m_sinksByName.end(); ++it ) {
+ if ( it.data() == dev ) { m_sinksByName.remove(it); break; }
+ }
+ for ( it = m_sourcesByName.begin(); it != m_sourcesByName.end(); ++it ) {
+ if ( it.data() == dev ) { m_sourcesByName.remove(it); break; }
+ }
+ // If a recording stream ended, decrement its parent source's count.
+ if ( ev->cat == AudioDevice::Recording ) {
+ TQMap<uint32_t,uint32_t>::Iterator si = m_sourceOutputToSource.find( ev->paIndex );
+ if ( si != m_sourceOutputToSource.end() ) {
+ PulseDevice *src = findDevice( m_sources, si.data() );
+ if ( src ) src->adjustRecordingCount( -1 );
+ m_sourceOutputToSource.remove( si );
+ }
+ }
emit deviceRemoved( dev );
- list->remove( dev ); // autoDelete=true, so dev is deleted here
+ list->remove( dev );
}
return;
}
// DeviceAdded or DeviceUpdated (both come through the same info callbacks).
+ // For Playback streams, resolve the parent sink's monitor source name so the
+ // level meter can attach a peak-detect stream to just this sink input.
+ TQString monitorName = ev->monitorName;
+ if ( ev->cat == AudioDevice::Playback && monitorName.isEmpty()
+ && ev->parentIndex != PA_INVALID_INDEX ) {
+ PulseDevice *sink = findDevice( m_sinks, ev->parentIndex );
+ if ( sink ) monitorName = sink->monitorName();
+ }
+
PulseDevice *dev = findDevice( *list, ev->paIndex );
if ( dev ) {
- dev->update( ev->name, ev->volume, ev->muted );
+ dev->update( ev->name, ev->volume, ev->muted, ev->channels, ev->pan, monitorName, ev->iconName );
+ if ( ev->cat == AudioDevice::Playback && ev->parentIndex != PA_INVALID_INDEX )
+ dev->setSinkIndex( ev->parentIndex );
} else {
dev = new PulseDevice( ev->cat, ev->paIndex, this );
+ dev->setModel( this );
dev->setPAContext( m_context, m_mainloop );
- dev->update( ev->name, ev->volume, ev->muted );
+ dev->update( ev->name, ev->volume, ev->muted, ev->channels, ev->pan, monitorName, ev->iconName );
+ if ( ev->cat == AudioDevice::Playback && ev->parentIndex != PA_INVALID_INDEX )
+ dev->setSinkIndex( ev->parentIndex );
+ if ( !ev->paName.isEmpty() ) {
+ dev->setPaName( ev->paName );
+ }
list->append( dev );
+ if ( !ev->paName.isEmpty() ) {
+ if ( ev->cat == AudioDevice::Output )
+ m_sinksByName.insert( ev->paName, dev );
+ else if ( ev->cat == AudioDevice::Input )
+ m_sourcesByName.insert( ev->paName, dev );
+ }
emit deviceAdded( dev );
+ if ( ev->cat == AudioDevice::Output && ev->paName == m_defaultSinkName )
+ emit defaultOutputChanged( dev );
+ if ( ev->cat == AudioDevice::Input && ev->paName == m_defaultSourceName )
+ emit defaultInputChanged( dev );
+ // Track which source a new recording stream is attached to.
+ if ( ev->cat == AudioDevice::Recording && ev->parentIndex != PA_INVALID_INDEX ) {
+ m_sourceOutputToSource.insert( ev->paIndex, ev->parentIndex );
+ PulseDevice *src = findDevice( m_sources, ev->parentIndex );
+ if ( src ) src->adjustRecordingCount( +1 );
+ }
}
}
+AudioDevice *PulseModel::defaultOutput() const
+{
+ if ( m_sinksByName.contains(m_defaultSinkName) )
+ return m_sinksByName[m_defaultSinkName];
+ return 0;
+}
+
+AudioDevice *PulseModel::defaultInput() const
+{
+ if ( m_sourcesByName.contains(m_defaultSourceName) )
+ return m_sourcesByName[m_defaultSourceName];
+ return 0;
+}
+
PulseDevice *PulseModel::findDevice( TQPtrList<PulseDevice> &list, uint32_t paIndex )
{
for ( TQPtrListIterator<PulseDevice> it(list); *it; ++it )
@@ -268,4 +623,66 @@ PulseDevice *PulseModel::findDevice( TQPtrList<PulseDevice> &list, uint32_t paIn
return 0;
}
+// ---- operations called from UI (main thread) ---------------------------------
+
+void PulseModel::setDefaultOutput( AudioDevice *dev )
+{
+ PulseDevice *pd = dynamic_cast<PulseDevice*>(dev);
+ if ( !pd || pd->paName().isEmpty() || !m_context || !m_mainloop ) return;
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_operation *op = pa_context_set_default_sink( m_context, pd->paName().utf8().data(), 0, 0 );
+ if ( op ) pa_operation_unref( op );
+ pa_threaded_mainloop_unlock( m_mainloop );
+}
+
+void PulseModel::setDefaultInput( AudioDevice *dev )
+{
+ PulseDevice *pd = dynamic_cast<PulseDevice*>(dev);
+ if ( !pd || pd->paName().isEmpty() || !m_context || !m_mainloop ) return;
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_operation *op = pa_context_set_default_source( m_context, pd->paName().utf8().data(), 0, 0 );
+ if ( op ) pa_operation_unref( op );
+ pa_threaded_mainloop_unlock( m_mainloop );
+}
+
+void PulseModel::moveSinkInputToSink( AudioDevice *playbackDev, AudioDevice *outputDev )
+{
+ PulseDevice *si = dynamic_cast<PulseDevice*>(playbackDev);
+ PulseDevice *sink = dynamic_cast<PulseDevice*>(outputDev);
+ if ( !si || !sink || !m_context || !m_mainloop ) return;
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_operation *op = pa_context_move_sink_input_by_index(
+ m_context, si->paIndex(), sink->paIndex(), 0, 0 );
+ if ( op ) pa_operation_unref( op );
+ pa_threaded_mainloop_unlock( m_mainloop );
+}
+
+const PulseCardInfo *PulseModel::card( uint32_t index ) const
+{
+ for ( TQValueList<PulseCardInfo>::ConstIterator it = m_cards.begin();
+ it != m_cards.end(); ++it )
+ if ( (*it).index == index )
+ return &(*it);
+ return 0;
+}
+
+PulseCardInfo *PulseModel::findCard( uint32_t index )
+{
+ for ( TQValueList<PulseCardInfo>::Iterator it = m_cards.begin();
+ it != m_cards.end(); ++it )
+ if ( it->index == index )
+ return &(*it);
+ return 0;
+}
+
+void PulseModel::setCardProfile( uint32_t cardIndex, const TQString &profileName )
+{
+ if ( !m_context || !m_mainloop || profileName.isEmpty() ) return;
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_operation *op = pa_context_set_card_profile_by_index(
+ m_context, cardIndex, profileName.utf8().data(), 0, 0 );
+ if ( op ) pa_operation_unref( op );
+ pa_threaded_mainloop_unlock( m_mainloop );
+}
+
#include "pulsemodel.moc"
diff --git a/src/model/pulsemodel.h b/src/model/pulsemodel.h
index b96f2b1..52e379f 100644
--- a/src/model/pulsemodel.h
+++ b/src/model/pulsemodel.h
@@ -2,6 +2,8 @@
#include <tqobject.h>
#include <tqptrlist.h>
+#include <tqmap.h>
+#include <tqvaluelist.h>
#include <pulse/pulseaudio.h>
@@ -9,6 +11,38 @@
class PulseDevice;
+// ---- card data structures ---------------------------------------------------
+
+struct PulseCardProfile {
+ TQString name;
+ TQString description;
+ bool available;
+};
+
+struct PulseCardPort {
+ TQString name;
+ TQString description;
+ int available; // pa_port_available_t: 0=unknown, 1=no, 2=yes
+ int direction; // PA_DIRECTION_INPUT=1, PA_DIRECTION_OUTPUT=2
+ uint32_t type; // pa_device_port_type_t
+ TQString availabilityGroup;
+};
+
+struct PulseCardInfo {
+ uint32_t index;
+ TQString name;
+ TQString description;
+ TQString activeProfile;
+ TQString vendor; // device.vendor.name
+ TQString product; // device.product.name
+ TQString formFactor; // device.form_factor
+ TQString busType; // device.bus (pci/usb/etc)
+ TQValueList<PulseCardProfile> profiles;
+ TQValueList<PulseCardPort> ports;
+};
+
+// ---- model ------------------------------------------------------------------
+
class PulseModel : public TQObject
{
TQ_OBJECT
@@ -21,6 +55,16 @@ public:
void close();
TQPtrList<AudioDevice> devices( AudioDevice::Category cat ) const;
+ AudioDevice *defaultOutput() const;
+ AudioDevice *defaultInput() const;
+
+ void setDefaultOutput( AudioDevice *dev );
+ void setDefaultInput( AudioDevice *dev );
+ void moveSinkInputToSink( AudioDevice *playbackDev, AudioDevice *outputDev );
+
+ TQValueList<PulseCardInfo> cards() const { return m_cards; }
+ const PulseCardInfo *card( uint32_t index ) const;
+ void setCardProfile( uint32_t cardIndex, const TQString &profileName );
protected:
void customEvent( TQCustomEvent *e );
@@ -28,14 +72,22 @@ protected:
signals:
void deviceAdded( AudioDevice *dev );
void deviceRemoved( AudioDevice *dev );
- void ready(); // emitted once initial enumeration is complete
+ void defaultOutputChanged( AudioDevice *dev );
+ void defaultInputChanged( AudioDevice *dev );
+ void ready();
+
+ void cardAdded( uint32_t index );
+ void cardRemoved( uint32_t index );
+ void cardUpdated( uint32_t index );
private:
static void contextStateCb( pa_context *c, void *userdata );
+ static void serverInfoCb( pa_context *c, const pa_server_info *info, 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 cardInfoCb( pa_context *c, const pa_card_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();
@@ -48,11 +100,19 @@ private:
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 );
+ TQMap<TQString, PulseDevice*> m_sinksByName;
+ TQMap<TQString, PulseDevice*> m_sourcesByName;
+ TQString m_defaultSinkName;
+ TQString m_defaultSourceName;
+ TQMap<uint32_t, uint32_t> m_sourceOutputToSource; // soIdx → sourceIdx
+
+ TQValueList<PulseCardInfo> m_cards;
+
+ PulseDevice *findDevice( TQPtrList<PulseDevice> &list, uint32_t paIndex );
+ PulseCardInfo *findCard( uint32_t index );
};
diff --git a/src/ui/balanceknob.cpp b/src/ui/balanceknob.cpp
new file mode 100644
index 0000000..1c2faa4
--- /dev/null
+++ b/src/ui/balanceknob.cpp
@@ -0,0 +1,124 @@
+#include "balanceknob.h"
+
+#include <tqpainter.h>
+#include <tqpixmap.h>
+#include <tqimage.h>
+#include <tqevent.h>
+#include <math.h>
+
+static const double DEG2RAD = M_PI / 180.0;
+static const double TRAVEL = 135.0; // degrees each side from centre
+
+BalanceKnob::BalanceKnob( int minVal, int maxVal, int val, TQWidget *parent )
+ : TQWidget(parent),
+ m_min(minVal), m_max(maxVal), m_value(val),
+ m_dragY(0), m_dragValue(0), m_dragging(false)
+{
+ setFixedSize( 30, 30 );
+ setBackgroundMode( TQt::NoBackground );
+}
+
+void BalanceKnob::setValue( int v )
+{
+ v = v < m_min ? m_min : v > m_max ? m_max : v;
+ if ( v == m_value ) return;
+ m_value = v;
+ update();
+ emit valueChanged( v );
+}
+
+void BalanceKnob::paintEvent( TQPaintEvent * )
+{
+ // 2× supersampling for smooth edges (TQt3 has no native AA)
+ const int S = 2;
+ int W = width() * S, H = height() * S;
+ int cx = W / 2, cy = H / 2;
+ int r = ( W < H ? W : H ) / 2 - S;
+
+ TQPixmap pm( W, H );
+ pm.fill( paletteBackgroundColor() );
+ TQPainter p( &pm );
+
+ double mid = ( m_min + m_max ) / 2.0;
+ double half = ( m_max - m_min ) / 2.0;
+
+ // ---- range track arc (270° from 7:30 clockwise to 4:30) -----------------
+ p.setPen( TQPen( TQColor(90, 90, 90), S * 2 ) );
+ p.setBrush( TQt::NoBrush );
+ p.drawArc( cx - r, cy - r, 2*r, 2*r, 225*16, -270*16 );
+
+ // ---- value arc (from 12-o'clock to current position) --------------------
+ double current_qt = 90.0 - ( (m_value - mid) / half ) * TRAVEL;
+ int spanAngle = (int)( (current_qt - 90.0) * 16.0 );
+ if ( spanAngle != 0 ) {
+ p.setPen( TQPen( TQColor(90, 140, 220), S * 2 ) );
+ p.drawArc( cx - r, cy - r, 2*r, 2*r, 90*16, spanAngle );
+ }
+
+ // ---- knob body -----------------------------------------------------------
+ int kr = r - S * 3;
+ p.setPen( TQt::NoPen );
+
+ p.setBrush( TQColor(38, 38, 42) );
+ p.drawEllipse( cx - kr, cy - kr, 2*kr, 2*kr );
+
+ p.setBrush( TQColor(68, 68, 74) );
+ p.drawEllipse( cx - kr + S, cy - kr + S, 2*kr - S*3, 2*kr - S*3 );
+
+ // ---- centre rest dot ----------------------------------------------------
+ p.setPen( TQt::NoPen );
+ p.setBrush( TQColor(100, 100, 110) );
+ p.drawEllipse( cx - S, cy - kr + S*2, S*2, S*2 );
+
+ // ---- indicator line -----------------------------------------------------
+ double angle_rad = ( (m_value - mid) / half ) * TRAVEL * DEG2RAD;
+ int ix = cx + (int)( (kr - S*3) * sin( angle_rad ) );
+ int iy = cy - (int)( (kr - S*3) * cos( angle_rad ) );
+
+ p.setPen( TQPen( TQt::white, S * 2 ) );
+ p.drawLine( cx, cy, ix, iy );
+
+ p.end();
+
+ // Scale back down and blit
+ TQImage scaled = pm.convertToImage().smoothScale( width(), height() );
+ TQPainter out( this );
+ out.drawImage( 0, 0, scaled );
+}
+
+void BalanceKnob::mousePressEvent( TQMouseEvent *e )
+{
+ if ( e->button() == TQt::LeftButton ) {
+ m_dragY = e->globalPos().y();
+ m_dragValue = m_value;
+ m_dragging = true;
+ grabMouse();
+ }
+}
+
+void BalanceKnob::mouseMoveEvent( TQMouseEvent *e )
+{
+ if ( !m_dragging ) return;
+ int delta = m_dragY - e->globalPos().y(); // drag up = increase
+ setValue( m_dragValue + delta );
+}
+
+void BalanceKnob::mouseReleaseEvent( TQMouseEvent * )
+{
+ if ( m_dragging ) {
+ m_dragging = false;
+ releaseMouse();
+ }
+}
+
+void BalanceKnob::mouseDoubleClickEvent( TQMouseEvent * )
+{
+ setValue( (int)( (m_min + m_max) / 2.0 ) );
+}
+
+void BalanceKnob::wheelEvent( TQWheelEvent *e )
+{
+ setValue( m_value + ( e->delta() > 0 ? 5 : -5 ) );
+}
+
+#include "balanceknob.moc"
diff --git a/src/ui/balanceknob.h b/src/ui/balanceknob.h
new file mode 100644
index 0000000..e8d0be2
--- /dev/null
+++ b/src/ui/balanceknob.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <tqwidget.h>
+
+class BalanceKnob : public TQWidget
+{
+ TQ_OBJECT
+
+public:
+ BalanceKnob( int minVal, int maxVal, int val, TQWidget *parent = 0 );
+
+ int value() const { return m_value; }
+ TQSize sizeHint() const { return TQSize(30, 30); }
+
+public slots:
+ void setValue( int v );
+
+signals:
+ void valueChanged( int v );
+
+protected:
+ void paintEvent( TQPaintEvent * );
+ void mousePressEvent( TQMouseEvent *e );
+ void mouseMoveEvent( TQMouseEvent *e );
+ void mouseReleaseEvent( TQMouseEvent *e );
+ void mouseDoubleClickEvent( TQMouseEvent *e );
+ void wheelEvent( TQWheelEvent *e );
+
+private:
+ int m_min, m_max, m_value;
+ int m_dragY;
+ int m_dragValue;
+ bool m_dragging;
+};
diff --git a/src/ui/devicespage.cpp b/src/ui/devicespage.cpp
new file mode 100644
index 0000000..28cce07
--- /dev/null
+++ b/src/ui/devicespage.cpp
@@ -0,0 +1,251 @@
+#include "devicespage.h"
+#include "../model/pulsemodel.h"
+
+#include <tqscrollview.h>
+#include <tqlayout.h>
+#include <tqlabel.h>
+#include <tqcombobox.h>
+#include <tqgroupbox.h>
+#include <tqstringlist.h>
+#include <tqpixmap.h>
+#include <tqbitmap.h>
+#include <tqpainter.h>
+#include <tdelocale.h>
+#include <tdeglobal.h>
+#include <kiconloader.h>
+
+#include <pulse/pulseaudio.h>
+
+static TQString portTypeIcon( uint32_t type )
+{
+ switch ( type ) {
+ case PA_DEVICE_PORT_TYPE_HEADPHONES: return "audio-headphones";
+ case PA_DEVICE_PORT_TYPE_HEADSET: return "audio-headset";
+ case PA_DEVICE_PORT_TYPE_MIC: return "audio-input-microphone";
+ case PA_DEVICE_PORT_TYPE_SPEAKER: return "audio-speakers";
+ case PA_DEVICE_PORT_TYPE_HDMI: return "video-display";
+ case PA_DEVICE_PORT_TYPE_SPDIF: return "audio-card";
+ case PA_DEVICE_PORT_TYPE_LINE: return "audio-card";
+ case PA_DEVICE_PORT_TYPE_USB: return "audio-card";
+ case PA_DEVICE_PORT_TYPE_BLUETOOTH: return "bluetooth";
+ default: return "audio-card";
+ }
+}
+
+static TQPixmap availDot( bool plugged, int size )
+{
+ TQPixmap pm( size, size );
+ TQBitmap mask( size, size, true );
+ {
+ TQPainter mp( &mask );
+ mp.setBrush( Qt::color1 );
+ mp.setPen( Qt::NoPen );
+ mp.drawEllipse( 1, 1, size-2, size-2 );
+ }
+ pm.setMask( mask );
+ pm.fill( plugged ? TQColor(80, 200, 80) : TQColor(160, 160, 160) );
+ {
+ TQPainter p( &pm );
+ p.setBrush( plugged ? TQColor(80, 200, 80) : TQColor(160, 160, 160) );
+ p.setPen( Qt::NoPen );
+ p.drawEllipse( 1, 1, size-2, size-2 );
+ }
+ return pm;
+}
+
+DevicesPage::DevicesPage( PulseModel *model, TQWidget *parent )
+ : TQWidget(parent), m_model(model), m_container(0)
+{
+ TQVBoxLayout *outer = new TQVBoxLayout( this, 0, 0 );
+ m_scroll = new TQScrollView( this );
+ m_scroll->setFrameStyle( TQFrame::NoFrame );
+ m_scroll->setResizePolicy( TQScrollView::AutoOneFit );
+ outer->addWidget( m_scroll );
+
+ rebuild();
+
+ connect( model, TQ_SIGNAL(cardAdded(uint32_t)), this, TQ_SLOT(onCardAdded(uint32_t)) );
+ connect( model, TQ_SIGNAL(cardRemoved(uint32_t)), this, TQ_SLOT(onCardRemoved(uint32_t)) );
+ connect( model, TQ_SIGNAL(cardUpdated(uint32_t)), this, TQ_SLOT(onCardUpdated(uint32_t)) );
+}
+
+void DevicesPage::rebuild()
+{
+ m_comboCard.clear();
+ m_profileNames.clear();
+ m_cardCombo.clear();
+ m_cardPorts.clear();
+
+ delete m_container;
+ m_container = new TQWidget( m_scroll->viewport() );
+ m_scroll->addChild( m_container );
+
+ TQVBoxLayout *vbox = new TQVBoxLayout( m_container, 8, 6 );
+
+ TQValueList<PulseCardInfo> cards = m_model->cards();
+ for ( TQValueList<PulseCardInfo>::Iterator it = cards.begin(); it != cards.end(); ++it ) {
+ PulseCardInfo &info = *it;
+
+ TQGroupBox *grp = new TQGroupBox( info.description, m_container );
+ grp->setColumnLayout( 0, Qt::Vertical );
+ grp->layout()->setSpacing( 4 );
+ grp->layout()->setMargin( 8 );
+ vbox->addWidget( grp );
+
+ TQGridLayout *grid = new TQGridLayout( grp->layout(), 0, 2, 4 );
+ grid->setColStretch( 1, 1 );
+ int gridRow = 0;
+
+ auto addRow = [&]( const TQString &labelText, TQWidget *valueWidget ) {
+ TQLabel *lbl = new TQLabel( labelText, grp );
+ lbl->setAlignment( TQt::AlignRight | TQt::AlignVCenter );
+ grid->addWidget( lbl, gridRow, 0 );
+ grid->addWidget( valueWidget, gridRow, 1 );
+ ++gridRow;
+ };
+ auto addTextRow = [&]( const TQString &labelText, const TQString &valueText ) {
+ addRow( labelText, new TQLabel( valueText, grp ) );
+ };
+
+ if ( !info.vendor.isEmpty() )
+ addTextRow( i18n("Vendor:"), info.vendor );
+ if ( !info.product.isEmpty() )
+ addTextRow( i18n("Product:"), info.product );
+ TQStringList typeparts;
+ if ( !info.formFactor.isEmpty() ) typeparts << info.formFactor;
+ if ( !info.busType.isEmpty() ) typeparts << info.busType;
+ if ( !typeparts.isEmpty() )
+ addTextRow( i18n("Type:"), typeparts.join(" / ") );
+
+ TQComboBox *combo = new TQComboBox( false, grp );
+ combo->setSizePolicy( TQSizePolicy::Preferred, TQSizePolicy::Fixed );
+ addRow( i18n("Profile:"), combo );
+
+ TQValueList<TQString> names;
+ int activeIdx = 0, i = 0;
+ for ( TQValueList<PulseCardProfile>::Iterator pit = info.profiles.begin();
+ pit != info.profiles.end(); ++pit, ++i ) {
+ combo->insertItem( pit->description );
+ names.append( pit->name );
+ if ( pit->name == info.activeProfile )
+ activeIdx = i;
+ }
+ if ( info.profiles.count() > 0 )
+ combo->setCurrentItem( activeIdx );
+
+ m_profileNames[combo] = names;
+ m_comboCard[combo] = info.index;
+ m_cardCombo[info.index] = combo;
+
+ connect( combo, TQ_SIGNAL(activated(int)), this, TQ_SLOT(onProfileActivated(int)) );
+
+ // Ports — 2-column grid to keep tall cards compact
+ if ( !info.ports.isEmpty() ) {
+ TQLabel *portsHdr = new TQLabel( i18n("Ports:"), grp );
+ portsHdr->setAlignment( TQt::AlignRight | TQt::AlignTop );
+ TQWidget *portsWidget = new TQWidget( grp );
+ TQGridLayout *portsGrid = new TQGridLayout( portsWidget, 0, 2, 0, 4 );
+ portsGrid->setColStretch( 0, 1 );
+ portsGrid->setColStretch( 1, 1 );
+
+ TQValueList<PortWidgets> portWidgetList;
+ int portIdx = 0;
+ for ( TQValueList<PulseCardPort>::Iterator pit = info.ports.begin();
+ pit != info.ports.end(); ++pit, ++portIdx ) {
+ bool plugged = ( pit->available != PA_PORT_AVAILABLE_NO );
+
+ TQWidget *row = new TQWidget( portsWidget );
+ TQHBoxLayout *hbox = new TQHBoxLayout( row, 0, 4 );
+
+ TQLabel *dot = new TQLabel( row );
+ dot->setPixmap( availDot( plugged, 10 ) );
+ dot->setFixedSize( 10, 10 );
+
+ TQLabel *icon = new TQLabel( row );
+ TQPixmap px = TDEGlobal::iconLoader()->loadIcon(
+ portTypeIcon( pit->type ), TDEIcon::Small, 16,
+ TDEIcon::DefaultState, 0, true );
+ if ( !px.isNull() )
+ icon->setPixmap( px );
+ icon->setFixedSize( 16, 16 );
+
+ TQLabel *name = new TQLabel( pit->description, row );
+
+ hbox->addWidget( dot );
+ hbox->addWidget( icon );
+ hbox->addWidget( name );
+ hbox->addStretch( 1 );
+
+ portsGrid->addWidget( row, portIdx / 2, portIdx % 2 );
+
+ PortWidgets pw;
+ pw.dot = dot;
+ portWidgetList.append( pw );
+ }
+ m_cardPorts[info.index] = portWidgetList;
+
+ grid->addWidget( portsHdr, gridRow, 0, TQt::AlignRight | TQt::AlignTop );
+ grid->addWidget( portsWidget, gridRow, 1 );
+ ++gridRow;
+ }
+ }
+
+ vbox->addStretch( 1 );
+ m_container->show();
+}
+
+void DevicesPage::onCardAdded( uint32_t ) { rebuild(); }
+void DevicesPage::onCardRemoved( uint32_t ) { rebuild(); }
+
+void DevicesPage::onCardUpdated( uint32_t index )
+{
+ TQMap<uint32_t, TQComboBox*>::Iterator cit = m_cardCombo.find( index );
+ if ( cit == m_cardCombo.end() ) return;
+ TQComboBox *combo = cit.data();
+
+ const PulseCardInfo *info = m_model->card( index );
+ if ( !info ) return;
+
+ // Update active profile in combo
+ TQMap<TQComboBox*, TQValueList<TQString> >::Iterator nit = m_profileNames.find( combo );
+ if ( nit != m_profileNames.end() ) {
+ TQValueList<TQString> &names = nit.data();
+ for ( int i = 0; i < (int)names.count(); ++i ) {
+ if ( names[i] == info->activeProfile ) {
+ combo->blockSignals( true );
+ combo->setCurrentItem( i );
+ combo->blockSignals( false );
+ break;
+ }
+ }
+ }
+
+ // Update port availability indicators in-place
+ TQMap<uint32_t, TQValueList<PortWidgets> >::Iterator pit = m_cardPorts.find( index );
+ if ( pit == m_cardPorts.end() ) return;
+ TQValueList<PortWidgets> &portWidgets = pit.data();
+
+ int i = 0;
+ for ( TQValueList<PulseCardPort>::ConstIterator p = info->ports.begin();
+ p != info->ports.end() && i < (int)portWidgets.count(); ++p, ++i ) {
+ bool plugged = ( (*p).available != PA_PORT_AVAILABLE_NO );
+ portWidgets[i].dot->setPixmap( availDot( plugged, 10 ) );
+ }
+}
+
+void DevicesPage::onProfileActivated( int idx )
+{
+ TQComboBox *combo = dynamic_cast<TQComboBox*>( const_cast<TQObject*>( sender() ) );
+ if ( !combo ) return;
+
+ TQMap<TQComboBox*, uint32_t>::Iterator cit = m_comboCard.find( combo );
+ if ( cit == m_comboCard.end() ) return;
+ uint32_t cardIndex = cit.data();
+
+ TQMap<TQComboBox*, TQValueList<TQString> >::Iterator nit = m_profileNames.find( combo );
+ if ( nit == m_profileNames.end() || idx >= (int)nit.data().count() ) return;
+
+ m_model->setCardProfile( cardIndex, nit.data()[idx] );
+}
+
+#include "devicespage.moc"
diff --git a/src/ui/devicespage.h b/src/ui/devicespage.h
new file mode 100644
index 0000000..49081f4
--- /dev/null
+++ b/src/ui/devicespage.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <tqwidget.h>
+#include <tqmap.h>
+#include <tqvaluelist.h>
+#include <tqstring.h>
+
+class PulseModel;
+class TQScrollView;
+class TQComboBox;
+class TQLabel;
+class TQVBoxLayout;
+
+struct PortWidgets {
+ TQLabel *dot;
+};
+
+class DevicesPage : public TQWidget
+{
+ TQ_OBJECT
+
+public:
+ explicit DevicesPage( PulseModel *model, TQWidget *parent = 0 );
+
+private slots:
+ void onCardAdded( uint32_t index );
+ void onCardRemoved( uint32_t index );
+ void onCardUpdated( uint32_t index );
+ void onProfileActivated( int comboIndex );
+
+private:
+ void rebuild();
+
+ PulseModel *m_model;
+ TQScrollView *m_scroll;
+ TQWidget *m_container;
+
+ TQMap<TQComboBox*, uint32_t> m_comboCard;
+ TQMap<TQComboBox*, TQValueList<TQString> > m_profileNames;
+ TQMap<uint32_t, TQComboBox*> m_cardCombo;
+ TQMap<uint32_t, TQValueList<PortWidgets> > m_cardPorts;
+};
diff --git a/src/ui/devicewidget.cpp b/src/ui/devicewidget.cpp
index 420d5fd..e6401d2 100644
--- a/src/ui/devicewidget.cpp
+++ b/src/ui/devicewidget.cpp
@@ -1,69 +1,383 @@
#include "devicewidget.h"
+#include "kledbutton.h"
+#include "levelmeter.h"
#include "../model/audiodevice.h"
+#include "../model/pulsedevice.h"
+#include "../model/pulsemodel.h"
-#include <tqlabel.h>
#include <tqlayout.h>
#include <tqslider.h>
-#include <tqtoolbutton.h>
+#include <tqlabel.h>
+#include <tqframe.h>
+#include <tqpainter.h>
+#include <tqtooltip.h>
+#include <tqevent.h>
+#include "balanceknob.h"
+#include <tqradiobutton.h>
+#include <kled.h>
+#include <kiconloader.h>
+#include <tdeglobal.h>
+#include <kstandarddirs.h>
+#include <tdepopupmenu.h>
+#include <tdelocale.h>
+
+static TQPixmap kmixPic( const char *name )
+{
+ TQString path = TDEGlobal::dirs()->findResource( "data",
+ TQString("tmix/pics/") + name );
+ return path.isEmpty() ? TQPixmap() : TQPixmap( path );
+}
+
+static TQPixmap kmixIconForDevice( const TQString &paIconName, AudioDevice::Category cat )
+{
+ // Input devices always get the mic icon — overrides device name hints.
+ if ( cat == AudioDevice::Input )
+ return kmixPic("mix_microphone.png");
+
+ // Map PA device icon name keywords to KMix pics before falling back to category.
+ TQString n = paIconName.lower();
+ if ( n.contains("headset") || n.contains("headphone") )
+ return kmixPic("mix_headphone.png");
+ if ( n.contains("microphone") || n.contains("mic") )
+ return kmixPic("mix_microphone.png");
+ if ( n.contains("digital") || n.contains("hdmi") || n.contains("iec") )
+ return kmixPic("mix_digital.png");
+ if ( n.contains("surround") )
+ return kmixPic("mix_surround.png");
+ if ( n.contains("cd") )
+ return kmixPic("mix_cd.png");
+ if ( n.contains("midi") )
+ return kmixPic("mix_midi.png");
+ if ( n.contains("video") )
+ return kmixPic("mix_video.png");
+
+ // Category fallback
+ switch ( cat ) {
+ case AudioDevice::Output: return kmixPic("mix_volume.png");
+ case AudioDevice::Input: return kmixPic("mix_microphone.png");
+ case AudioDevice::Playback: return kmixPic("mix_audio.png");
+ case AudioDevice::Recording: return kmixPic("mix_record.png");
+ }
+ return TQPixmap();
+}
+
+// Rotated label — text reads bottom-to-top. Mirrors KMix VerticalText.
+class VerticalLabel : public TQWidget
+{
+public:
+ VerticalLabel( const TQString &text, TQWidget *parent )
+ : TQWidget(parent, text.utf8()), m_text(text)
+ {
+ resize( 20, 100 );
+ setMinimumSize( 20, 10 );
+ }
+
+ void setText( const TQString &t ) { m_text = t; update(); }
+
+ TQSize sizeHint() const { return TQSize( 20, 100 ); }
+
+ TQSizePolicy sizePolicy() const
+ {
+ return TQSizePolicy( TQSizePolicy::Fixed, TQSizePolicy::Expanding );
+ }
+
+protected:
+ void paintEvent( TQPaintEvent * )
+ {
+ TQPainter p( this );
+ TQFontMetrics fm = p.fontMetrics();
+ int available = height() - 6;
+ TQString text = m_text;
+ if ( fm.width(text) > available ) {
+ while ( text.length() > 1 && fm.width(text) + fm.width("...") > available )
+ text.truncate( text.length() - 1 );
+ text += "...";
+ }
+ p.rotate( 270 );
+ p.translate( 0, -4 );
+ p.drawText( -height() + 2, width(), text );
+ }
+
+private:
+ TQString m_text;
+};
+
-DeviceWidget::DeviceWidget( AudioDevice *device, TQWidget *parent )
- : TQWidget(parent), m_device(device)
+DeviceWidget::DeviceWidget( AudioDevice *device, PulseModel *model, TQWidget *parent )
+ : TQWidget(parent), m_device(device), m_model(model),
+ m_sep(0), m_recordingLed(0), m_defaultBtn(0)
{
- TQVBoxLayout *layout = new TQVBoxLayout( this, 4, 2 );
+ // ---- widgets -------------------------------------------------------------
+ m_label = new VerticalLabel( device->name(), this );
- m_label = new TQLabel( device->name(), this );
- m_label->setAlignment( TQLabel::AlignHCenter );
+ // App / device icon — 22px, loaded from TDE icon theme with KMix pic fallbacks.
+ m_iconLabel = new TQLabel( this );
+ m_iconLabel->setFixedSize( 22, 22 );
+ m_iconLabel->setAlignment( TQLabel::AlignCenter );
+ {
+ TQString icon = device->iconName();
+ TQPixmap px;
+ if ( !icon.isEmpty() )
+ px = TDEGlobal::iconLoader()->loadIcon(
+ icon, TDEIcon::Small, 22,
+ TDEIcon::DefaultState, 0, true /*canReturnNull*/ );
+ if ( px.isNull() )
+ px = kmixIconForDevice( icon, device->category() );
+ if ( !px.isNull() )
+ m_iconLabel->setPixmap( px );
+ }
- m_slider = new TQSlider( 0, 100, 5, device->volume(), TQt::Vertical, this );
- m_slider->setTickmarks( TQSlider::NoMarks );
+ m_volLabel = new TQLabel( TQString::number(device->volume()) + "%", this );
+ m_volLabel->setAlignment( TQLabel::AlignHCenter );
- m_muteButton = new TQToolButton( this );
- m_muteButton->setToggleButton( true );
- m_muteButton->setTextLabel( "M" );
- m_muteButton->setOn( device->muted() );
+ m_levelMeter = new LevelMeter( this );
+ m_levelMeter->setFixedWidth( 14 );
- layout->addWidget( m_label );
- layout->addWidget( m_slider, 1 );
- layout->addWidget( m_muteButton, 0, TQt::AlignHCenter );
+ m_volSlider = new TQSlider( 0, 100, 5, 100 - device->volume(), TQt::Vertical, this );
+ m_volSlider->setTickmarks( TQSlider::Right );
+ m_volSlider->setTickInterval( 5 );
- // slider → device
- connect( m_slider, TQ_SIGNAL(valueChanged(int)), this, TQ_SLOT(onVolumeChanged(int)) );
- connect( m_muteButton, TQ_SIGNAL(clicked()), this, TQ_SLOT(onMuteToggled()) );
+ // ---- layout --------------------------------------------------------------
- // device → widget
- connect( device, TQ_SIGNAL(volumeChanged(int)), this, TQ_SLOT(onDeviceVolume(int)) );
- connect( device, TQ_SIGNAL(muteChanged(bool)), this, TQ_SLOT(onDeviceMute(bool)) );
+ // top row: icon + vol% — centered over the meter+slider column (not the label)
+ TQHBoxLayout *topRow = new TQHBoxLayout( 0, 0, 2 );
+ topRow->addStretch( 1 );
+ topRow->addWidget( m_iconLabel, 0, TQt::AlignVCenter );
+ topRow->addSpacing( 3 );
+ topRow->addWidget( m_volLabel, 0, TQt::AlignVCenter );
+ topRow->addStretch( 1 );
+
+ // meter + slider column, with topRow sitting above them
+ TQHBoxLayout *meterSlider = new TQHBoxLayout( 0, 0, 2 );
+ meterSlider->addWidget( m_levelMeter );
+ meterSlider->addWidget( m_volSlider, 1 );
+
+ TQVBoxLayout *sliderCol = new TQVBoxLayout( 0, 0, 2 );
+ sliderCol->addLayout( topRow, 0 );
+ sliderCol->addLayout( meterSlider, 1 );
+
+ // strip: vertical label beside the slider column
+ TQHBoxLayout *strip = new TQHBoxLayout( 0, 0, 2 );
+ strip->addWidget( m_label );
+ strip->addLayout( sliderCol, 1 );
+
+ // bottom row — fixed 30px height so all widgets align regardless of content.
+ // Created before its child widgets so they can be parented directly to it.
+ TQWidget *bottomWidget = new TQWidget( this );
+ bottomWidget->setFixedHeight( 30 );
+ TQHBoxLayout *bottom = new TQHBoxLayout( bottomWidget, 0, 2 );
+
+ // Balance knob — not meaningful for Input (just attenuates capture channels)
+ if ( device->category() != AudioDevice::Input ) {
+ m_balanceDial = new BalanceKnob( -50, 50, device->pan(), bottomWidget );
+ TQToolTip::add( m_balanceDial, i18n("Balance (drag or scroll; double-click to center)") );
+ } else {
+ m_balanceDial = 0;
+ }
+
+ // Red recording-active LED — Input only, shows when a stream is capturing
+ if ( device->category() == AudioDevice::Input ) {
+ m_recordingLed = new KLed( TQt::red, KLed::Off, KLed::Raised, KLed::Circular, bottomWidget );
+ m_recordingLed->setFixedSize( 16, 16 );
+ TQToolTip::add( m_recordingLed, i18n("Microphone in use") );
+ connect( device, TQ_SIGNAL(recordingActiveChanged(bool)),
+ this, TQ_SLOT(onRecordingActive(bool)) );
+ }
+
+ // Green LED = live, off = muted. Click to toggle.
+ m_muteLed = new KLedButton(
+ TQt::green,
+ device->muted() ? KLed::Off : KLed::On,
+ KLed::Raised, KLed::Circular, bottomWidget );
+ m_muteLed->setFixedSize( 16, 16 );
+ TQToolTip::add( m_muteLed, i18n("Mute") );
+
+ // Radio button = this device is the system default. Only for Output/Input.
+ AudioDevice::Category cat = device->category();
+ if ( model && ( cat == AudioDevice::Output || cat == AudioDevice::Input ) ) {
+ m_defaultBtn = new TQRadioButton( TQString(), bottomWidget );
+ TQToolTip::add( m_defaultBtn, i18n("Set as default") );
+ connect( m_defaultBtn, TQ_SIGNAL(clicked()), this, TQ_SLOT(onDefaultClicked()) );
+
+ if ( cat == AudioDevice::Output ) {
+ connect( model, TQ_SIGNAL(defaultOutputChanged(AudioDevice*)),
+ this, TQ_SLOT(onDefaultChanged(AudioDevice*)) );
+ onDefaultChanged( model->defaultOutput() );
+ } else {
+ connect( model, TQ_SIGNAL(defaultInputChanged(AudioDevice*)),
+ this, TQ_SLOT(onDefaultChanged(AudioDevice*)) );
+ onDefaultChanged( model->defaultInput() );
+ }
+ }
+ bottom->addStretch( 1 );
+ if ( m_defaultBtn )
+ bottom->addWidget( m_defaultBtn, 0, TQt::AlignVCenter );
+ bottom->addStretch( 1 );
+ if ( m_balanceDial )
+ bottom->addWidget( m_balanceDial, 0, TQt::AlignVCenter );
+ else if ( m_recordingLed )
+ bottom->addWidget( m_recordingLed, 0, TQt::AlignVCenter );
+ bottom->addStretch( 1 );
+ bottom->addWidget( m_muteLed, 0, TQt::AlignVCenter );
+ bottom->addStretch( 1 );
+
+ // outer column: strip (label+sliderCol) | bottom row
+ TQVBoxLayout *right = new TQVBoxLayout( 0, 0, 2 );
+ right->addLayout( strip, 1 );
+ right->addWidget( bottomWidget, 0 );
+
+ // separator on right edge
+ m_sep = new TQFrame( this );
+ m_sep->setFrameStyle( TQFrame::VLine | TQFrame::Sunken );
+ m_sep->setFixedWidth( 4 );
+
+ TQHBoxLayout *outer = new TQHBoxLayout( this, 4, 2 );
+ outer->addLayout( right, 1 );
+ outer->addWidget( m_sep, 0, TQt::AlignVCenter );
+
+ // ---- connections ---------------------------------------------------------
+ connect( m_volSlider, TQ_SIGNAL(valueChanged(int)), this, TQ_SLOT(onVolumeChanged(int)) );
+ if ( m_balanceDial )
+ connect( m_balanceDial, TQ_SIGNAL(valueChanged(int)), this, TQ_SLOT(onBalanceChanged(int)) );
+ connect( m_muteLed, TQ_SIGNAL(stateChanged(bool)), this, TQ_SLOT(onMuteClicked()) );
+
+ 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(panChanged(int)), this, TQ_SLOT(onDevicePan(int)) );
+ connect( device, TQ_SIGNAL(levelChanged(float)), this, TQ_SLOT(onDeviceLevel(float)) );
connect( device, TQ_SIGNAL(nameChanged(const TQString&)), this, TQ_SLOT(onDeviceName(const TQString&)) );
}
void DeviceWidget::onVolumeChanged( int v )
{
- m_device->setVolume( v );
+ int pct = 100 - v;
+ m_volLabel->setText( TQString::number(pct) + "%" );
+ m_device->setVolume( pct );
}
-void DeviceWidget::onMuteToggled()
-{
- m_device->setMuted( !m_device->muted() );
-}
+void DeviceWidget::onBalanceChanged( int v ) { m_device->setPan( v ); }
+void DeviceWidget::onMuteClicked() { m_device->setMuted( m_muteLed->state() == KLed::Off ); }
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 );
+ m_volSlider->blockSignals( true );
+ m_volSlider->setValue( 100 - v );
+ m_volSlider->blockSignals( false );
+ m_volLabel->setText( TQString::number(v) + "%" );
}
void DeviceWidget::onDeviceMute( bool m )
{
- m_muteButton->blockSignals( true );
- m_muteButton->setOn( m );
- m_muteButton->blockSignals( false );
+ m_muteLed->blockSignals( true );
+ m_muteLed->setState( m ? KLed::Off : KLed::On );
+ m_muteLed->blockSignals( false );
+}
+
+void DeviceWidget::onDevicePan( int p )
+{
+ m_balanceDial->blockSignals( true );
+ m_balanceDial->setValue( p );
+ m_balanceDial->blockSignals( false );
}
+void DeviceWidget::onDeviceLevel( float level ) { m_levelMeter->setLevel( level ); }
+
void DeviceWidget::onDeviceName( const TQString &name )
{
- m_label->setText( name );
+ static_cast<VerticalLabel*>(m_label)->setText( name );
+}
+
+void DeviceWidget::contextMenuEvent( TQContextMenuEvent *e )
+{
+ TDEPopupMenu menu( this );
+ menu.insertTitle( m_device->name() );
+
+ int muteId = menu.insertItem( i18n("Mute"), this, TQ_SLOT(onToggleMute()) );
+ menu.setItemChecked( muteId, m_device->muted() );
+
+ if ( m_model ) {
+ AudioDevice::Category cat = m_device->category();
+
+ if ( cat == AudioDevice::Output ) {
+ menu.insertSeparator();
+ menu.insertItem( i18n("Set as Default Output"), this, TQ_SLOT(onSetDefault()) );
+
+ } else if ( cat == AudioDevice::Input ) {
+ menu.insertSeparator();
+ menu.insertItem( i18n("Set as Default Input"), this, TQ_SLOT(onSetDefault()) );
+
+ } else if ( cat == AudioDevice::Playback ) {
+ TQPtrList<AudioDevice> sinks = m_model->devices( AudioDevice::Output );
+ if ( !sinks.isEmpty() ) {
+ menu.insertTitle( i18n("Move to Sink") );
+ m_moveTargets.clear();
+
+ PulseDevice *pd = dynamic_cast<PulseDevice*>(m_device);
+ uint32_t currentSink = pd ? pd->sinkIndex() : PA_INVALID_INDEX;
+
+ int idx = 0;
+ for ( TQPtrListIterator<AudioDevice> it(sinks); *it; ++it ) {
+ m_moveTargets.append( *it );
+ int itemId = menu.insertItem( (*it)->name(), 1000 + idx );
+ PulseDevice *sink = dynamic_cast<PulseDevice*>(*it);
+ if ( sink && sink->paIndex() == currentSink )
+ menu.setItemChecked( itemId, true );
+ idx++;
+ }
+ connect( &menu, TQ_SIGNAL(activated(int)),
+ this, TQ_SLOT(onMoveToSink(int)) );
+ }
+ }
+ }
+
+ menu.exec( e->globalPos() );
+ e->accept();
+}
+
+void DeviceWidget::onToggleMute()
+{
+ m_device->setMuted( !m_device->muted() );
+}
+
+void DeviceWidget::onDefaultClicked()
+{
+ if ( !m_model ) return;
+ if ( m_device->category() == AudioDevice::Output )
+ m_model->setDefaultOutput( m_device );
+ else if ( m_device->category() == AudioDevice::Input )
+ m_model->setDefaultInput( m_device );
+}
+
+void DeviceWidget::onDefaultChanged( AudioDevice *dev )
+{
+ if ( !m_defaultBtn ) return;
+ m_defaultBtn->blockSignals( true );
+ m_defaultBtn->setChecked( dev == m_device );
+ m_defaultBtn->blockSignals( false );
+}
+
+void DeviceWidget::onRecordingActive( bool active )
+{
+ if ( m_recordingLed )
+ m_recordingLed->setState( active ? KLed::On : KLed::Off );
+}
+
+void DeviceWidget::onSetDefault()
+{
+ onDefaultClicked();
+}
+
+void DeviceWidget::setSeparatorVisible( bool v )
+{
+ if ( m_sep ) v ? m_sep->show() : m_sep->hide();
+}
+
+void DeviceWidget::onMoveToSink( int id )
+{
+ if ( !m_model ) return;
+ int idx = id - 1000;
+ if ( idx < 0 || (uint)idx >= m_moveTargets.count() ) return;
+ m_model->moveSinkInputToSink( m_device, m_moveTargets.at(idx) );
}
#include "devicewidget.moc"
diff --git a/src/ui/devicewidget.h b/src/ui/devicewidget.h
index 4a6cead..a5532bf 100644
--- a/src/ui/devicewidget.h
+++ b/src/ui/devicewidget.h
@@ -2,33 +2,66 @@
#include <tqwidget.h>
#include <tqstring.h>
+#include <tqpixmap.h>
+#include <tqptrlist.h>
class AudioDevice;
+class PulseModel;
+class KLed;
+class KLedButton;
+class LevelMeter;
class TQSlider;
-class TQToolButton;
+class BalanceKnob;
class TQLabel;
-class TQVBoxLayout;
+class TQRadioButton;
+class TQContextMenuEvent;
+class TQFrame;
class DeviceWidget : public TQWidget
{
TQ_OBJECT
public:
- DeviceWidget( AudioDevice *device, TQWidget *parent = 0 );
+ DeviceWidget( AudioDevice *device, PulseModel *model, TQWidget *parent = 0 );
~DeviceWidget() {}
AudioDevice *device() const { return m_device; }
+ LevelMeter *levelMeter() const { return m_levelMeter; }
+ void setSeparatorVisible( bool v );
+
+protected:
+ void contextMenuEvent( TQContextMenuEvent *e );
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 onVolumeChanged( int v );
+ void onBalanceChanged( int v );
+ void onMuteClicked();
+ void onToggleMute();
+ void onSetDefault();
+ void onDefaultClicked();
+ void onMoveToSink( int id );
+ void onDefaultChanged( AudioDevice *dev );
+ void onRecordingActive( bool active );
+
+ void onDeviceVolume( int v );
+ void onDeviceMute( bool m );
+ void onDevicePan( int p );
+ void onDeviceLevel( float level );
void onDeviceName( const TQString &name );
private:
- AudioDevice *m_device;
- TQSlider *m_slider;
- TQToolButton *m_muteButton;
- TQLabel *m_label;
+ AudioDevice *m_device;
+ PulseModel *m_model;
+ TQFrame *m_sep;
+ TQSlider *m_volSlider;
+ BalanceKnob *m_balanceDial;
+ KLedButton *m_muteLed;
+ KLed *m_recordingLed; // null unless Input
+ TQRadioButton *m_defaultBtn; // null for Playback/Recording
+ TQLabel *m_volLabel;
+ LevelMeter *m_levelMeter;
+ TQWidget *m_label;
+ TQLabel *m_iconLabel;
+
+ TQPtrList<AudioDevice> m_moveTargets; // populated by context menu build
};
diff --git a/src/ui/kledbutton.cpp b/src/ui/kledbutton.cpp
new file mode 100644
index 0000000..f3b5ecc
--- /dev/null
+++ b/src/ui/kledbutton.cpp
@@ -0,0 +1,29 @@
+#include "kledbutton.h"
+#include <tqsizepolicy.h>
+
+KLedButton::KLedButton( const TQColor &col, TQWidget *parent, const char *name )
+ : KLed( col, parent, name )
+{
+}
+
+KLedButton::KLedButton( const TQColor &col, KLed::State st, KLed::Look look,
+ KLed::Shape shape, TQWidget *parent, const char *name )
+ : KLed( col, st, look, shape, parent, name )
+{
+}
+
+void KLedButton::mousePressEvent( TQMouseEvent *e )
+{
+ if ( e->button() == TQt::LeftButton ) {
+ toggle();
+ emit stateChanged( state() == KLed::On );
+ }
+}
+
+TQSize KLedButton::sizeHint() const { return TQSize( 22, 22 ); }
+TQSizePolicy KLedButton::sizePolicy() const
+{
+ return TQSizePolicy( TQSizePolicy::Fixed, TQSizePolicy::Fixed );
+}
+
+#include "kledbutton.moc"
diff --git a/src/ui/kledbutton.h b/src/ui/kledbutton.h
new file mode 100644
index 0000000..3bdadaa
--- /dev/null
+++ b/src/ui/kledbutton.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <tqwidget.h>
+#include <kled.h>
+
+class KLedButton : public KLed
+{
+ TQ_OBJECT
+
+public:
+ KLedButton( const TQColor &col = TQt::green, TQWidget *parent = 0, const char *name = 0 );
+ KLedButton( const TQColor &col, KLed::State st, KLed::Look look, KLed::Shape shape,
+ TQWidget *parent = 0, const char *name = 0 );
+ ~KLedButton() {}
+
+ TQSize sizeHint() const;
+ TQSizePolicy sizePolicy() const;
+
+signals:
+ void stateChanged( bool newState );
+
+protected:
+ void mousePressEvent( TQMouseEvent *e );
+};
diff --git a/src/ui/levelmeter.cpp b/src/ui/levelmeter.cpp
new file mode 100644
index 0000000..52412de
--- /dev/null
+++ b/src/ui/levelmeter.cpp
@@ -0,0 +1,67 @@
+#include "levelmeter.h"
+#include <tqpainter.h>
+#include <math.h>
+
+LevelMeter::LevelMeter( TQWidget *parent )
+ : TQWidget(parent), m_level(0.0f)
+{
+ setBackgroundMode( NoBackground );
+}
+
+// Map linear 0..1 amplitude to dB display position 0..1 over a 60dB range.
+// -60dB = bottom, 0dB = top. Matches how real VU meters look.
+static float toDisplayLevel( float linear )
+{
+ if ( linear < 0.001f ) return 0.0f;
+ float db = 20.0f * log10f( linear ); // e.g. 0.3 → -10.5 dB
+ float pos = ( db + 60.0f ) / 60.0f; // -60..0 dB → 0..1
+ if ( pos < 0.0f ) pos = 0.0f;
+ if ( pos > 1.0f ) pos = 1.0f;
+ return pos;
+}
+
+void LevelMeter::setLevel( float level )
+{
+ if ( level < 0.0f ) level = 0.0f;
+ if ( level > 1.0f ) level = 1.0f;
+
+ float display = toDisplayLevel( level );
+ if ( display > m_level )
+ m_level = m_level * 0.3f + display * 0.7f; // fast attack, not instant
+ else {
+ m_level *= 0.94f; // slow decay ~1.5s to floor
+ if ( m_level < 0.001f ) m_level = 0.0f;
+ }
+ update();
+}
+
+TQSize LevelMeter::sizeHint() const
+{
+ return TQSize( 8, 48 );
+}
+
+void LevelMeter::paintEvent( TQPaintEvent * )
+{
+ TQPainter p( this );
+ int w = width();
+ int h = height();
+
+ p.fillRect( 0, 0, w, h, TQColor( 40, 40, 40 ) );
+
+ if ( m_level <= 0.0f ) return;
+
+ struct Zone { float lo, hi; TQColor col; };
+ static const Zone zones[3] = {
+ { 0.00f, 0.70f, TQColor( 50, 200, 50 ) },
+ { 0.70f, 0.90f, TQColor( 230, 200, 0 ) },
+ { 0.90f, 1.00f, TQColor( 220, 40, 40 ) },
+ };
+
+ for ( int z = 0; z < 3; z++ ) {
+ if ( m_level <= zones[z].lo ) break;
+ float segEnd = ( m_level < zones[z].hi ) ? m_level : zones[z].hi;
+ int yTop = h - (int)( segEnd * h );
+ int yBot = h - (int)( zones[z].lo * h );
+ p.fillRect( 0, yTop, w, yBot - yTop, zones[z].col );
+ }
+}
diff --git a/src/ui/levelmeter.h b/src/ui/levelmeter.h
new file mode 100644
index 0000000..012c6f6
--- /dev/null
+++ b/src/ui/levelmeter.h
@@ -0,0 +1,17 @@
+#pragma once
+#include <tqwidget.h>
+
+class LevelMeter : public TQWidget
+{
+public:
+ explicit LevelMeter( TQWidget *parent = 0 );
+
+ void setLevel( float level ); // 0.0 – 1.0, fast attack / slow decay
+ TQSize sizeHint() const;
+
+protected:
+ void paintEvent( TQPaintEvent * );
+
+private:
+ float m_level;
+};
diff --git a/src/ui/mixerwindow.cpp b/src/ui/mixerwindow.cpp
index 91cea65..caa6af5 100644
--- a/src/ui/mixerwindow.cpp
+++ b/src/ui/mixerwindow.cpp
@@ -1,44 +1,174 @@
#include "mixerwindow.h"
+#include "preferencesdlg.h"
#include "../model/pulsemodel.h"
#include <tqlayout.h>
#include <tqscrollview.h>
+#include <tqwidgetstack.h>
+#include <tqtoolbutton.h>
+#include <tqtooltip.h>
+#include <tqevent.h>
#include <ktabwidget.h>
#include <tdelocale.h>
+#include <kiconloader.h>
+#include <tdeglobal.h>
+#include <kstandarddirs.h>
+#include <tdeaction.h>
+#include <tdeactioncollection.h>
+#include <tdemenubar.h>
+#include <tdepopupmenu.h>
+#include <tdeaboutapplication.h>
+#include <tdeaboutdata.h>
+#include <tdeapplication.h>
+#include "tmixtray.h"
+#include "devicespage.h"
+#include <tdeconfig.h>
+#include <tdemessagebox.h>
+#include <kstdguiitem.h>
+
+// ---- tab helper -------------------------------------------------------------
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 );
+
+ TQScrollView *sv = new TQScrollView( tabs );
+ sv->setFrameStyle( TQFrame::NoFrame );
+ sv->setHScrollBarMode( TQScrollView::Auto );
+ sv->setVScrollBarMode( TQScrollView::AlwaysOff );
+ sv->setResizePolicy( TQScrollView::AutoOneFit );
+
+ t.page = new TQWidget( sv->viewport() );
+ sv->addChild( t.page, 0, 0 );
+
t.layout = new TQHBoxLayout( t.page, 6, 4 );
+ t.layout->addStretch( 1 );
+ t.scroll = sv;
+ t.count = 0;
- tabs->addTab( scroll, label );
+ tabs->addTab( sv, label );
return t;
}
MixerWindow::MixerWindow( PulseModel *model, TQWidget *parent )
- : TQWidget(parent), m_model(model)
+ : TDEMainWindow(parent), m_model(model), m_prefsDlg(0),
+ m_devicesBtn(0), m_devicesPage(0), m_devicesInTabs(false), m_quitting(false)
{
- setCaption( i18n("TMix") );
+ setPlainCaption( i18n("Volume Control") );
+ setIcon( TDEGlobal::iconLoader()->loadIcon( "kmix", TDEIcon::Desktop, 48 ) );
+
+ // ---- menu bar -----------------------------------------------------------
+ TDEPopupMenu *fileMenu = new TDEPopupMenu( this );
+ fileMenu->insertItem( SmallIcon("application-exit"), i18n("&Quit"), this, TQ_SLOT(quit()) );
+
+ TDEPopupMenu *settingsMenu = new TDEPopupMenu( this );
+ settingsMenu->insertItem( SmallIcon("configure"), i18n("&Configure TMix..."),
+ this, TQ_SLOT(showPreferences()) );
+
+ TDEPopupMenu *helpMenu = new TDEPopupMenu( this );
+ helpMenu->insertItem( SmallIcon("help"), i18n("&About TMix"), this, TQ_SLOT(showAbout()) );
+
+ menuBar()->insertItem( i18n("&File"), fileMenu );
+ menuBar()->insertItem( i18n("&Settings"), settingsMenu );
+ menuBar()->insertItem( i18n("&Help"), helpMenu );
+
+ // ---- system tray --------------------------------------------------------
+ m_tray = new TmixTray( this );
+ m_tray->setModel( model );
+ connect( model, TQ_SIGNAL(defaultOutputChanged(AudioDevice*)),
+ this, TQ_SLOT(onDefaultOutputChanged(AudioDevice*)) );
+ onDefaultOutputChanged( model->defaultOutput() );
+
+ TDEConfig *cfg = TDEGlobal::config();
+ cfg->setGroup("General");
+ if ( cfg->readBoolEntry("DockInTray", true) )
+ m_tray->show();
+
+ // ---- central widget -----------------------------------------------------
+ TQWidget *central = new TQWidget( this );
+ setCentralWidget( central );
+ setMinimumSize( 300, 200 );
+
+ TQVBoxLayout *top = new TQVBoxLayout( central, 0, 0 );
- TQVBoxLayout *top = new TQVBoxLayout( this, 4, 0 );
- m_tabs = new KTabWidget( this );
- top->addWidget( m_tabs );
+ // Outer stack: page 0 = mixer content, page 1 = devices (no-tab mode only)
+ m_outerStack = new TQWidgetStack( central );
+ top->addWidget( m_outerStack, 1 );
+
+ TQWidget *mixerPage = new TQWidget( m_outerStack );
+ TQVBoxLayout *mixerLayout = new TQVBoxLayout( mixerPage, 0, 0 );
+ m_outerStack->addWidget( mixerPage, 0 );
+ m_outerStack->raiseWidget( 0 );
+
+ m_stack = new TQWidgetStack( mixerPage );
+ mixerLayout->addWidget( m_stack );
+
+ // Page 0: tabbed view
+ m_tabs = new KTabWidget( m_stack );
+ m_stack->addWidget( m_tabs, 0 );
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") );
+ // Page 1: all-in-one flat view (scrollable)
+ {
+ TQScrollView *sv = new TQScrollView( m_stack );
+ sv->setFrameStyle( TQFrame::NoFrame );
+ sv->setHScrollBarMode( TQScrollView::Auto );
+ sv->setVScrollBarMode( TQScrollView::AlwaysOff );
+ sv->setResizePolicy( TQScrollView::AutoOneFit );
+ m_allPage = sv;
+
+ m_allStrip = new TQWidget( sv->viewport() );
+ sv->addChild( m_allStrip, 0, 0 );
+
+ m_allLayout = new TQHBoxLayout( m_allStrip, 2, 4 );
+ m_allLayout->addStretch( 1 );
+
+ m_allCount = 0;
+ m_stack->addWidget( sv, 1 );
+ }
+
+ // Floating toggle button — child of central widget so TQWidgetStack page
+ // switches don't hide it; positioned over m_outerStack's bottom-right corner.
+ m_devicesBtn = new TQToolButton( central );
+ m_devicesBtn->setIconSet( SmallIconSet("configure") );
+ m_devicesBtn->resize( 24, 24 );
+ TQToolTip::add( m_devicesBtn, i18n("Device Configuration") );
+ connect( m_devicesBtn, TQ_SIGNAL(clicked()), this, TQ_SLOT(toggleDevices()) );
+ central->installEventFilter( this );
+
+ // DevicesPage — placement depends on tab/no-tab mode (set below)
+ m_devicesPage = new DevicesPage( model, this );
+
+ {
+ TDEConfig *c = TDEGlobal::config();
+ c->setGroup("View");
+ bool noTabs = c->readBoolEntry("NoTabs", false);
+ m_stack->raiseWidget( noTabs ? 1 : 0 );
+ // Place DevicesPage in the right home for the initial mode
+ if ( noTabs ) {
+ m_devicesPage->reparent( m_outerStack, 0, TQPoint(), false );
+ m_outerStack->addWidget( m_devicesPage, 1 );
+ m_devicesInTabs = false;
+ } else {
+ m_devicesPage->reparent( m_tabs, 0, TQPoint(), false );
+ m_tabs->addTab( m_devicesPage, i18n("Devices") );
+ m_devicesInTabs = true;
+ }
+ }
+
+ setAutoSaveSettings( "MainWindow" );
+
+ // Position button after layout is set; hide if in tab mode (has its own tab)
+ repositionDevicesBtn();
+ m_devicesBtn->setShown( !m_devicesInTabs );
+
connect( model, TQ_SIGNAL(deviceAdded(AudioDevice*)), this, TQ_SLOT(onDeviceAdded(AudioDevice*)) );
connect( model, TQ_SIGNAL(deviceRemoved(AudioDevice*)), this, TQ_SLOT(onDeviceRemoved(AudioDevice*)) );
+ connect( tqApp, TQ_SIGNAL(aboutToQuit()), this, TQ_SLOT(onAppAboutToQuit()) );
// Populate with any devices already known at construction time.
AudioDevice::Category cats[] = {
@@ -52,6 +182,37 @@ MixerWindow::MixerWindow( PulseModel *model, TQWidget *parent )
}
}
+bool MixerWindow::queryClose()
+{
+ if ( m_quitting || tdeApp->sessionSaving() )
+ return true;
+ TDEConfig *cfg = TDEGlobal::config();
+ cfg->setGroup("General");
+ if ( cfg->readBoolEntry("DockInTray", true) ) {
+ saveMainWindowSettings( TDEGlobal::config(), "MainWindow" );
+ TDEConfig *c = TDEGlobal::config();
+ c->setGroup("MainWindow");
+ c->writeEntry("XPos", x());
+ c->writeEntry("YPos", y());
+ c->sync();
+ hide();
+ return false;
+ }
+ return true;
+}
+
+
+void MixerWindow::showAbout()
+{
+ TDEAboutData about(
+ "tmix", I18N_NOOP("TMix"), "0.1",
+ I18N_NOOP("Volume control for TDE / PulseAudio"),
+ TDEAboutData::License_GPL,
+ "(c) 2026", 0, 0, 0 );
+ TDEAboutApplication dlg( &about, this );
+ dlg.exec();
+}
+
MixerWindow::Tab &MixerWindow::tabForCategory( AudioDevice::Category cat )
{
switch ( cat ) {
@@ -65,21 +226,199 @@ MixerWindow::Tab &MixerWindow::tabForCategory( AudioDevice::Category cat )
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 );
+ if ( m_stack->id( m_stack->visibleWidget() ) == 1 ) {
+ TQWidget *w = dev->createWidget( m_allStrip );
+ m_allLayout->insertWidget( m_allCount, w );
+ m_allCount++;
+ w->show();
+ m_widgets.insert( dev, w );
+ } else {
+ Tab &t = tabForCategory( dev->category() );
+ TQWidget *w = dev->createWidget( t.page );
+ t.layout->insertWidget( t.count, w );
+ t.count++;
+ w->show();
+ m_widgets.insert( dev, w );
+ }
+}
+
+
+void MixerWindow::onDefaultOutputChanged( AudioDevice *dev )
+{
+ m_tray->setDevice( dev );
}
void MixerWindow::onDeviceRemoved( AudioDevice *dev )
{
TQWidget *w = m_widgets[dev];
if ( !w ) return;
- Tab &t = tabForCategory( dev->category() );
- t.layout->remove( w );
+ if ( m_stack->id( m_stack->visibleWidget() ) == 1 ) {
+ m_allLayout->remove( w );
+ m_allCount--;
+ } else {
+ Tab &t = tabForCategory( dev->category() );
+ t.layout->remove( w );
+ t.count--;
+ }
m_widgets.remove( dev );
delete w;
}
+
+void MixerWindow::quit()
+{
+ TDEConfig *cfg = TDEGlobal::config();
+ cfg->setGroup("General");
+ if ( cfg->readBoolEntry("ConfirmQuit", false) ) {
+ int r = KMessageBox::questionYesNo(
+ this,
+ i18n("Are you sure you want to quit TMix?"),
+ i18n("Quit TMix"),
+ KStdGuiItem::quit(),
+ KStdGuiItem::cancel() );
+ if ( r != KMessageBox::Yes )
+ return;
+ }
+ m_quitting = true;
+ tqApp->quit();
+}
+
+void MixerWindow::onAppAboutToQuit()
+{
+ m_quitting = true;
+}
+
+void MixerWindow::showPreferences()
+{
+ if ( !m_prefsDlg ) {
+ m_prefsDlg = new PreferencesDlg( this );
+ connect( m_prefsDlg, TQ_SIGNAL(settingsChanged()), this, TQ_SLOT(applySettings()) );
+ }
+ m_prefsDlg->load();
+ m_prefsDlg->show();
+ m_prefsDlg->raise();
+ m_prefsDlg->setActiveWindow();
+}
+
+void MixerWindow::rebuildView()
+{
+ // Tear down all existing device widgets
+ TQMap<AudioDevice*, TQWidget*>::iterator it;
+ for ( it = m_widgets.begin(); it != m_widgets.end(); ++it )
+ delete it.data();
+ m_widgets.clear();
+ m_output.count = m_input.count = m_playback.count = m_recording.count = 0;
+ m_allCount = 0;
+
+ TDEConfig *cfg = TDEGlobal::config();
+ cfg->setGroup("View");
+ bool noTabs = cfg->readBoolEntry("NoTabs", false);
+ m_stack->raiseWidget( noTabs ? 1 : 0 );
+
+ if ( noTabs && m_devicesInTabs ) {
+ // Move DevicesPage from m_tabs → m_outerStack
+ m_tabs->removePage( m_devicesPage );
+ m_devicesPage->reparent( m_outerStack, 0, TQPoint(), false );
+ m_outerStack->addWidget( m_devicesPage, 1 );
+ m_outerStack->raiseWidget( 0 );
+ updateDevicesButton();
+ m_devicesInTabs = false;
+ m_devicesBtn->show();
+ repositionDevicesBtn();
+ } else if ( !noTabs && !m_devicesInTabs ) {
+ // Move DevicesPage from m_outerStack → m_tabs
+ m_outerStack->removeWidget( m_devicesPage );
+ m_devicesPage->reparent( m_tabs, 0, TQPoint(), false );
+ m_tabs->addTab( m_devicesPage, i18n("Devices") );
+ m_outerStack->raiseWidget( 0 );
+ m_devicesInTabs = true;
+ m_devicesBtn->hide();
+ }
+
+ // Repopulate into whichever view is now active
+ AudioDevice::Category cats[] = {
+ AudioDevice::Output, AudioDevice::Input,
+ AudioDevice::Playback, AudioDevice::Recording
+ };
+ for ( int i = 0; i < 4; i++ ) {
+ TQPtrList<AudioDevice> devs = m_model->devices( cats[i] );
+ for ( TQPtrListIterator<AudioDevice> jt(devs); *jt; ++jt )
+ onDeviceAdded( *jt );
+ }
+}
+
+
+void MixerWindow::applySettings()
+{
+ TDEConfig *cfg = TDEGlobal::config();
+
+ cfg->setGroup("General");
+ if ( cfg->readBoolEntry("DockInTray", true) )
+ m_tray->show();
+ else
+ m_tray->hide();
+
+ cfg->setGroup("View");
+ bool noTabs = cfg->readBoolEntry("NoTabs", false);
+ if ( noTabs != ( m_stack->id( m_stack->visibleWidget() ) == 1 ) ) {
+ rebuildView();
+ } else {
+ // Tabs mode: apply per-tab visibility
+ m_tabs->setTabEnabled( m_output.scroll, cfg->readBoolEntry("ShowOutput", true) );
+ m_tabs->setTabEnabled( m_input.scroll, cfg->readBoolEntry("ShowInput", true) );
+ m_tabs->setTabEnabled( m_playback.scroll, cfg->readBoolEntry("ShowPlayback", true) );
+ m_tabs->setTabEnabled( m_recording.scroll, cfg->readBoolEntry("ShowRecording", true) );
+ }
+}
+
+void MixerWindow::toggleDevices()
+{
+ int next = ( m_outerStack->id( m_outerStack->visibleWidget() ) == 0 ) ? 1 : 0;
+ m_outerStack->raiseWidget( next );
+ updateDevicesButton();
+ repositionDevicesBtn();
+ m_devicesBtn->raise();
+}
+
+void MixerWindow::updateDevicesButton()
+{
+ if ( m_outerStack->id( m_outerStack->visibleWidget() ) == 0 ) {
+ m_devicesBtn->setIconSet( SmallIconSet("configure") );
+ TQToolTip::add( m_devicesBtn, i18n("Device Configuration") );
+ } else {
+ m_devicesBtn->setIconSet( SmallIconSet("kmix") );
+ TQToolTip::add( m_devicesBtn, i18n("Mixer") );
+ }
+}
+
+void MixerWindow::repositionDevicesBtn()
+{
+ int bw = m_devicesBtn->width() ? m_devicesBtn->width() : 24;
+ int bh = m_devicesBtn->height() ? m_devicesBtn->height() : 24;
+ // Position in bottom-right of m_outerStack, mapped to centralWidget coords
+ TQPoint origin = m_outerStack->mapTo( centralWidget(), TQPoint(0, 0) );
+ int x = origin.x() + m_outerStack->width() - bw - 4;
+ int y = origin.y() + m_outerStack->height() - bh - 4;
+ m_devicesBtn->move( x, y );
+ m_devicesBtn->raise();
+}
+
+bool MixerWindow::eventFilter( TQObject *obj, TQEvent *e )
+{
+ if ( obj == centralWidget() && e->type() == TQEvent::Resize )
+ repositionDevicesBtn();
+ return false;
+}
+
+void MixerWindow::showEvent( TQShowEvent *e )
+{
+ TDEMainWindow::showEvent( e );
+ repositionDevicesBtn();
+
+ TDEConfig *c = TDEGlobal::config();
+ c->setGroup("MainWindow");
+ if ( c->hasKey("XPos") && c->hasKey("YPos") )
+ move( c->readNumEntry("XPos"), c->readNumEntry("YPos") );
+}
+
#include "mixerwindow.moc"
diff --git a/src/ui/mixerwindow.h b/src/ui/mixerwindow.h
index f6b7be8..c472246 100644
--- a/src/ui/mixerwindow.h
+++ b/src/ui/mixerwindow.h
@@ -1,15 +1,19 @@
#pragma once
-#include <tqwidget.h>
#include <tqmap.h>
+#include <tdemainwindow.h>
#include "../model/audiodevice.h"
class PulseModel;
class KTabWidget;
class TQHBoxLayout;
+class TQWidgetStack;
+class TQToolButton;
+class DevicesPage;
+class TQResizeEvent;
-class MixerWindow : public TQWidget
+class MixerWindow : public TDEMainWindow
{
TQ_OBJECT
@@ -17,22 +21,49 @@ public:
explicit MixerWindow( PulseModel *model, TQWidget *parent = 0 );
~MixerWindow() {}
-private slots:
- void onDeviceAdded( AudioDevice *dev );
- void onDeviceRemoved( AudioDevice *dev );
-
-public:
struct Tab {
+ TQWidget *scroll;
TQWidget *page;
TQHBoxLayout *layout;
+ int count;
};
-private:
+protected:
+ bool queryClose();
+ bool eventFilter( TQObject *obj, TQEvent *e );
+ void showEvent( TQShowEvent *e );
+private slots:
+ void onDeviceAdded( AudioDevice *dev );
+ void onDeviceRemoved( AudioDevice *dev );
+ void onDefaultOutputChanged( AudioDevice *dev );
+ void showAbout();
+ void showPreferences();
+ void applySettings();
+ void quit();
+ void onAppAboutToQuit();
+ void toggleDevices();
+
+private:
Tab &tabForCategory( AudioDevice::Category cat );
+ void rebuildView();
+ void updateDevicesButton();
+ void repositionDevicesBtn();
- PulseModel *m_model;
- KTabWidget *m_tabs;
+ PulseModel *m_model;
+ KTabWidget *m_tabs;
+ class PreferencesDlg *m_prefsDlg;
+ TQWidgetStack *m_outerStack;
+ TQWidgetStack *m_stack;
+ TQWidget *m_allPage;
+ TQWidget *m_allStrip;
+ TQHBoxLayout *m_allLayout;
+ int m_allCount;
+ class TmixTray *m_tray;
+ TQToolButton *m_devicesBtn;
+ DevicesPage *m_devicesPage;
+ bool m_devicesInTabs; // true = DevicesPage is a tab in m_tabs
+ bool m_quitting;
Tab m_output;
Tab m_input;
diff --git a/src/ui/preferencesdlg.cpp b/src/ui/preferencesdlg.cpp
new file mode 100644
index 0000000..b6df120
--- /dev/null
+++ b/src/ui/preferencesdlg.cpp
@@ -0,0 +1,111 @@
+#include "preferencesdlg.h"
+
+#include <tqlayout.h>
+#include <tqcheckbox.h>
+#include <tqspinbox.h>
+#include <tqlabel.h>
+#include <tqframe.h>
+#include <tdeglobal.h>
+#include <tdeconfig.h>
+#include <tdelocale.h>
+
+PreferencesDlg::PreferencesDlg( TQWidget *parent )
+ : KDialogBase( Tabbed, i18n("Configure TMix"),
+ Ok | Apply | Cancel, Ok, parent )
+{
+ // ---- General -------------------------------------------------------------
+ TQFrame *gen = addPage( i18n("General") );
+ TQVBoxLayout *gl = new TQVBoxLayout( gen, marginHint(), spacingHint() );
+
+ m_dockInTray = new TQCheckBox( i18n("Dock in system tray"), gen );
+ gl->addWidget( m_dockInTray );
+
+ m_showPopup = new TQCheckBox( i18n("Show mini volume popup on tray click"), gen );
+ gl->addWidget( m_showPopup );
+
+ m_showRecTray = new TQCheckBox( i18n("Show microphone-in-use icon in tray"), gen );
+ gl->addWidget( m_showRecTray );
+
+ m_confirmQuit = new TQCheckBox( i18n("Ask for confirmation before quitting"), gen );
+ gl->addWidget( m_confirmQuit );
+
+
+ gl->addSpacing( 8 );
+
+ TQHBoxLayout *stepRow = new TQHBoxLayout( 0, 0, spacingHint() );
+ stepRow->addWidget( new TQLabel( i18n("Scroll wheel volume step:"), gen ) );
+ m_scrollStep = new TQSpinBox( 1, 20, 1, gen );
+ m_scrollStep->setSuffix( i18n(" %") );
+ stepRow->addWidget( m_scrollStep );
+ stepRow->addStretch();
+ gl->addLayout( stepRow );
+
+ gl->addStretch();
+
+ // ---- View ----------------------------------------------------------------
+ TQFrame *view = addPage( i18n("View") );
+ TQVBoxLayout *vl = new TQVBoxLayout( view, marginHint(), spacingHint() );
+
+ m_noTabs = new TQCheckBox( i18n("Show all devices in one view (no tabs)"), view );
+ vl->addWidget( m_noTabs );
+
+ vl->addSpacing( 8 );
+ vl->addWidget( new TQLabel( i18n("Show tabs:"), view ) );
+ m_showOutput = new TQCheckBox( i18n("Output"), view );
+ m_showInput = new TQCheckBox( i18n("Input"), view );
+ m_showPlayback = new TQCheckBox( i18n("Playback"), view );
+ m_showRecording = new TQCheckBox( i18n("Recording"), view );
+ vl->addWidget( m_showOutput );
+ vl->addWidget( m_showInput );
+ vl->addWidget( m_showPlayback );
+ vl->addWidget( m_showRecording );
+ vl->addStretch();
+
+ load();
+}
+
+void PreferencesDlg::load()
+{
+ TDEConfig *cfg = TDEGlobal::config();
+
+ cfg->setGroup("General");
+ m_dockInTray->setChecked( cfg->readBoolEntry("DockInTray", true) );
+ m_showPopup->setChecked( cfg->readBoolEntry("ShowPopup", true) );
+ m_showRecTray->setChecked( cfg->readBoolEntry("ShowRecordingTray", true) );
+ m_confirmQuit->setChecked( cfg->readBoolEntry("ConfirmQuit", false) );
+ m_scrollStep->setValue( cfg->readNumEntry( "ScrollStep", 5) );
+
+ cfg->setGroup("View");
+ m_noTabs->setChecked( cfg->readBoolEntry("NoTabs", false) );
+ m_showOutput->setChecked( cfg->readBoolEntry("ShowOutput", true) );
+ m_showInput->setChecked( cfg->readBoolEntry("ShowInput", true) );
+ m_showPlayback->setChecked( cfg->readBoolEntry("ShowPlayback", true) );
+ m_showRecording->setChecked( cfg->readBoolEntry("ShowRecording", true) );
+}
+
+void PreferencesDlg::save()
+{
+ TDEConfig *cfg = TDEGlobal::config();
+
+ cfg->setGroup("General");
+ cfg->writeEntry( "DockInTray", m_dockInTray->isChecked() );
+ cfg->writeEntry( "ShowPopup", m_showPopup->isChecked() );
+ cfg->writeEntry( "ShowRecordingTray", m_showRecTray->isChecked() );
+ cfg->writeEntry( "ConfirmQuit", m_confirmQuit->isChecked() );
+ cfg->writeEntry( "ScrollStep", m_scrollStep->value() );
+
+ cfg->setGroup("View");
+ cfg->writeEntry( "NoTabs", m_noTabs->isChecked() );
+ cfg->writeEntry( "ShowOutput", m_showOutput->isChecked() );
+ cfg->writeEntry( "ShowInput", m_showInput->isChecked() );
+ cfg->writeEntry( "ShowPlayback", m_showPlayback->isChecked() );
+ cfg->writeEntry( "ShowRecording", m_showRecording->isChecked() );
+
+ cfg->sync();
+ emit settingsChanged();
+}
+
+void PreferencesDlg::slotOk() { save(); accept(); }
+void PreferencesDlg::slotApply() { save(); }
+
+#include "preferencesdlg.moc"
diff --git a/src/ui/preferencesdlg.h b/src/ui/preferencesdlg.h
new file mode 100644
index 0000000..94f5682
--- /dev/null
+++ b/src/ui/preferencesdlg.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <kdialogbase.h>
+
+class TQCheckBox;
+class TQSpinBox;
+
+class PreferencesDlg : public KDialogBase
+{
+ TQ_OBJECT
+
+public:
+ PreferencesDlg( TQWidget *parent = 0 );
+ void load();
+
+signals:
+ void settingsChanged();
+
+protected slots:
+ void slotOk();
+ void slotApply();
+
+private:
+ void save();
+
+ TQCheckBox *m_dockInTray;
+ TQCheckBox *m_showPopup;
+ TQCheckBox *m_showRecTray;
+ TQCheckBox *m_confirmQuit;
+ TQSpinBox *m_scrollStep;
+
+ TQCheckBox *m_noTabs;
+ TQCheckBox *m_showOutput;
+ TQCheckBox *m_showInput;
+ TQCheckBox *m_showPlayback;
+ TQCheckBox *m_showRecording;
+};
diff --git a/src/ui/tmixapp.cpp b/src/ui/tmixapp.cpp
new file mode 100644
index 0000000..628e475
--- /dev/null
+++ b/src/ui/tmixapp.cpp
@@ -0,0 +1,20 @@
+#include "tmixapp.h"
+#include "mixerwindow.h"
+
+TmixApp::TmixApp() : KUniqueApplication(), m_win(0), m_firstInstance(true) {}
+
+int TmixApp::newInstance()
+{
+ if ( m_firstInstance ) {
+ m_firstInstance = false;
+ return 0;
+ }
+ if ( m_win ) {
+ m_win->show();
+ m_win->raise();
+ m_win->setActiveWindow();
+ }
+ return 0;
+}
+
+#include "tmixapp.moc"
diff --git a/src/ui/tmixapp.h b/src/ui/tmixapp.h
new file mode 100644
index 0000000..8f41245
--- /dev/null
+++ b/src/ui/tmixapp.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <kuniqueapplication.h>
+
+class MixerWindow;
+
+class TmixApp : public KUniqueApplication
+{
+ TQ_OBJECT
+public:
+ TmixApp();
+ int newInstance();
+ void setMainWindow( MixerWindow *w ) { m_win = w; }
+private:
+ MixerWindow *m_win;
+ bool m_firstInstance;
+};
diff --git a/src/ui/tmixpopup.cpp b/src/ui/tmixpopup.cpp
new file mode 100644
index 0000000..068c66e
--- /dev/null
+++ b/src/ui/tmixpopup.cpp
@@ -0,0 +1,134 @@
+#include "tmixpopup.h"
+#include "devicewidget.h"
+#include "../model/audiodevice.h"
+#include "../model/pulsemodel.h"
+
+#include <tqlayout.h>
+#include <tqframe.h>
+#include <tqlabel.h>
+#include <tqpushbutton.h>
+#include <tqapplication.h>
+#include <tqdesktopwidget.h>
+#include <tdelocale.h>
+#include <tdeconfig.h>
+#include <tdeglobal.h>
+#include <twin.h>
+
+TmixPopup::TmixPopup( PulseModel *model, TQWidget *parent )
+ : TQWidget( parent, "TmixPopup",
+ WStyle_Customize | WType_Popup | WStyle_DialogBorder ),
+ m_model(model), m_devWidget(0)
+{
+ // Single frame fills the popup — header and content share the same border
+ TQVBoxLayout *outer = new TQVBoxLayout( this, 0, 0 );
+
+ TQFrame *frame = new TQFrame( this );
+ frame->setFrameStyle( TQFrame::NoFrame );
+ outer->addWidget( frame );
+
+ m_layout = new TQVBoxLayout( frame, 0, 0 );
+
+ // Thin title strip — inside the frame, so edges line up exactly
+ TQLabel *header = new TQLabel( i18n("Volume"), frame );
+ header->setAlignment( TQt::AlignCenter );
+ header->setFixedHeight( 23 );
+ header->setPaletteBackgroundColor( palette().active().mid() );
+ header->setPaletteForegroundColor( palette().active().text() );
+ m_layout->addWidget( header );
+
+ // Spacer between header and device widget
+ m_layout->addSpacing( 4 );
+
+ // device widget inserted at index 0 by setDevice()
+
+ m_layout->addSpacing( 4 );
+
+ // "Mixer" button — flat, compact
+ TQPushButton *btn = new TQPushButton( i18n("Mixer"), frame );
+ btn->setFlat( true );
+ btn->setFixedWidth( 70 );
+ m_layout->addWidget( btn, 0, TQt::AlignHCenter );
+ m_layout->addSpacing( 4 );
+ connect( btn, TQ_SIGNAL(clicked()), this, TQ_SIGNAL(showMixerRequested()) );
+
+ connect( model, TQ_SIGNAL(defaultOutputChanged(AudioDevice*)),
+ this, TQ_SLOT(onDefaultOutputChanged(AudioDevice*)) );
+
+ setDevice( model->defaultOutput() );
+}
+
+void TmixPopup::setDevice( AudioDevice *dev )
+{
+ if ( m_devWidget ) {
+ m_layout->remove( m_devWidget );
+ delete m_devWidget;
+ m_devWidget = 0;
+ }
+ if ( !dev ) return;
+
+ m_devWidget = new DeviceWidget( dev, m_model,
+ static_cast<TQWidget*>( m_layout->parent() ) );
+ m_devWidget->setSeparatorVisible( false );
+ m_layout->insertWidget( 2, m_devWidget );
+ m_devWidget->show();
+ adjustSize();
+}
+
+void TmixPopup::onDefaultOutputChanged( AudioDevice *dev )
+{
+ setDevice( dev );
+}
+
+void TmixPopup::toggleAt( const TQPoint &trayPos, const TQSize &traySize )
+{
+ if ( isVisible() ) {
+ hide();
+ return;
+ }
+
+ adjustSize();
+
+ TQRect screen = TQApplication::desktop()->screenGeometry( trayPos );
+
+ int x = trayPos.x() + traySize.width() / 2 - width() / 2;
+ int y = trayPos.y() - height();
+
+ if ( y < screen.top() )
+ y = trayPos.y() + traySize.height();
+
+ if ( x + width() > screen.right() )
+ x = screen.right() - width();
+ if ( x < screen.left() )
+ x = screen.left();
+
+ move( x, y );
+ show();
+
+ KWin::setState( winId(), NET::KeepAbove | NET::SkipTaskbar | NET::SkipPager );
+ KWin::setOnAllDesktops( winId(), true );
+}
+
+bool TmixPopup::justHidden() const
+{
+ return m_hideTime.isValid() && m_hideTime.elapsed() < 300;
+}
+
+void TmixPopup::hideEvent( TQHideEvent *e )
+{
+ TQWidget::hideEvent( e );
+ m_hideTime.start();
+}
+
+void TmixPopup::wheelEvent( TQWheelEvent *e )
+{
+ AudioDevice *dev = m_model->defaultOutput();
+ if ( !dev ) return;
+ TDEConfig *cfg = TDEGlobal::config();
+ cfg->setGroup("General");
+ int step = cfg->readNumEntry("ScrollStep", 5);
+ int delta = e->delta() > 0 ? step : -step;
+ int vol = TQMAX( 0, TQMIN( 100, dev->volume() + delta ) );
+ dev->setVolume( vol );
+}
+
+#include "tmixpopup.moc"
diff --git a/src/ui/tmixpopup.h b/src/ui/tmixpopup.h
new file mode 100644
index 0000000..4dbdc9a
--- /dev/null
+++ b/src/ui/tmixpopup.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <tqwidget.h>
+#include <tqdatetime.h>
+
+class PulseModel;
+class AudioDevice;
+class DeviceWidget;
+class TQVBoxLayout;
+class TQPushButton;
+
+class TmixPopup : public TQWidget
+{
+ TQ_OBJECT
+
+public:
+ explicit TmixPopup( PulseModel *model, TQWidget *parent = 0 );
+
+ // Position relative to tray icon and show, or hide if already visible.
+ void toggleAt( const TQPoint &trayGlobalPos, const TQSize &traySize );
+
+ // True for ~300ms after hiding — prevents re-show on the same click.
+ bool justHidden() const;
+
+signals:
+ void showMixerRequested();
+
+protected:
+ void hideEvent( TQHideEvent *e );
+ void wheelEvent( TQWheelEvent *e );
+
+private slots:
+ void onDefaultOutputChanged( AudioDevice *dev );
+
+private:
+ void setDevice( AudioDevice *dev );
+
+ PulseModel *m_model;
+ DeviceWidget *m_devWidget;
+ TQVBoxLayout *m_layout;
+ TQTime m_hideTime;
+};
diff --git a/src/ui/tmixtray.cpp b/src/ui/tmixtray.cpp
new file mode 100644
index 0000000..c0980f0
--- /dev/null
+++ b/src/ui/tmixtray.cpp
@@ -0,0 +1,182 @@
+#include "tmixtray.h"
+#include "tmixpopup.h"
+#include "../model/audiodevice.h"
+#include "../model/pulsemodel.h"
+
+#include <tqevent.h>
+#include <tqpixmap.h>
+#include <tqstringlist.h>
+#include <tqtooltip.h>
+#include <tdeglobal.h>
+#include <kiconloader.h>
+#include <kstandarddirs.h>
+#include <tdelocale.h>
+#include <tdeconfig.h>
+#include <tdepopupmenu.h>
+
+TmixTray::TmixTray( TQWidget *parent )
+ : KSystemTray(parent), m_model(0), m_device(0), m_recTray(0),
+ m_popup(0), m_recordingCount(0)
+{
+ setCaption( i18n("Volume Control") );
+
+ TQPixmap titleIcon = TDEGlobal::iconLoader()->loadIcon(
+ "kmix", TDEIcon::Small, 16, TDEIcon::DefaultState, 0, true );
+ if ( titleIcon.isNull() )
+ titleIcon = TDEGlobal::iconLoader()->loadIcon(
+ "audio-volume-medium", TDEIcon::Small, 16, TDEIcon::DefaultState, 0, true );
+ contextMenu()->changeTitle( contextMenu()->idAt(0), titleIcon, i18n("Volume Control") );
+
+ updateIcon();
+}
+
+void TmixTray::setDevice( AudioDevice *dev )
+{
+ if ( m_device )
+ disconnect( m_device, 0, this, 0 );
+ m_device = dev;
+ if ( m_device ) {
+ connect( m_device, TQ_SIGNAL(volumeChanged(int)), this, TQ_SLOT(updateIcon()) );
+ connect( m_device, TQ_SIGNAL(muteChanged(bool)), this, TQ_SLOT(updateIcon()) );
+ }
+ updateIcon();
+}
+
+void TmixTray::setModel( PulseModel *model )
+{
+ m_model = model;
+ TQPtrList<AudioDevice> inputs = model->devices( AudioDevice::Input );
+ for ( TQPtrListIterator<AudioDevice> it(inputs); *it; ++it ) {
+ connect( *it, TQ_SIGNAL(recordingActiveChanged(bool)),
+ this, TQ_SLOT(onRecordingActive(bool)) );
+ }
+ connect( model, TQ_SIGNAL(deviceAdded(AudioDevice*)),
+ this, TQ_SLOT(onDeviceAdded(AudioDevice*)) );
+}
+
+void TmixTray::mousePressEvent( TQMouseEvent *e )
+{
+ if ( e->button() == TQt::LeftButton ) {
+ if ( !m_model ) { KSystemTray::mousePressEvent(e); return; }
+
+ TDEConfig *cfg = TDEGlobal::config();
+ cfg->setGroup("General");
+ bool showPopup = cfg->readBoolEntry("ShowPopup", true);
+
+ if ( showPopup ) {
+ if ( !m_popup ) {
+ m_popup = new TmixPopup( m_model, this );
+ connect( m_popup, TQ_SIGNAL(showMixerRequested()),
+ parentWidget(), TQ_SLOT(show()) );
+ }
+ if ( m_popup->justHidden() ) { e->accept(); return; }
+ m_popup->toggleAt( mapToGlobal( TQPoint(0,0) ), size() );
+ } else {
+ TQWidget *win = parentWidget();
+ win->isVisible() ? win->hide() : win->show();
+ }
+ e->accept();
+ return;
+ }
+ KSystemTray::mousePressEvent( e );
+}
+
+void TmixTray::updateIcon()
+{
+ int vol = m_device ? m_device->volume() : 0;
+ bool mute = m_device ? m_device->muted() : true;
+
+ const char *pic;
+ if ( mute || vol == 0 ) pic = "audio-volume-muted.png";
+ else if ( vol < 34 ) pic = "audio-volume-low.png";
+ else if ( vol < 67 ) pic = "audio-volume-medium.png";
+ else pic = "audio-volume-high.png";
+
+ TQString path = TDEGlobal::dirs()->findResource( "data",
+ TQString("tmix/pics/crystal/") + pic );
+ if ( !path.isEmpty() )
+ setPixmap( TQPixmap(path) );
+}
+
+void TmixTray::onRecordingActive( bool active )
+{
+ m_recordingCount += active ? 1 : -1;
+ if ( m_recordingCount < 0 ) m_recordingCount = 0;
+ updateRecordingTray();
+}
+
+void TmixTray::onDeviceAdded( AudioDevice *dev )
+{
+ if ( dev->category() == AudioDevice::Input )
+ connect( dev, TQ_SIGNAL(recordingActiveChanged(bool)),
+ this, TQ_SLOT(onRecordingActive(bool)) );
+}
+
+void TmixTray::updateRecordingTray()
+{
+ TDEConfig *cfg = TDEGlobal::config();
+ cfg->setGroup("General");
+ bool showRec = cfg->readBoolEntry("ShowRecordingTray", true);
+
+ if ( m_recordingCount > 0 && showRec ) {
+ if ( !m_recTray ) {
+ m_recTray = new KSystemTray( parentWidget() );
+ m_recTray->setCaption( i18n("Microphone Active") );
+
+ TQString path = TDEGlobal::dirs()->findResource( "data",
+ "tmix/pics/mix_microphone_recording.png" );
+ if ( path.isEmpty() )
+ path = TDEGlobal::dirs()->findResource( "data",
+ "tmix/pics/mix_microphone.png" );
+
+ if ( !path.isEmpty() )
+ m_recTray->setPixmap( TQPixmap( path ) );
+ }
+
+ // Build tooltip: list names of active recording streams
+ if ( m_model ) {
+ TQStringList names;
+ TQPtrList<AudioDevice> streams = m_model->devices( AudioDevice::Recording );
+ for ( TQPtrListIterator<AudioDevice> it(streams); *it; ++it )
+ names.append( (*it)->name() );
+ TQString tip = names.isEmpty()
+ ? i18n("Microphone in use")
+ : i18n("Recording: ") + names.join(", ");
+ TQToolTip::add( m_recTray, tip );
+ }
+
+ m_recTray->show();
+ } else if ( m_recTray ) {
+ m_recTray->hide();
+ }
+}
+
+void TmixTray::wheelEvent( TQWheelEvent *e )
+{
+ if ( !m_device ) return;
+ TDEConfig *cfg = TDEGlobal::config();
+ cfg->setGroup("General");
+ int step = cfg->readNumEntry("ScrollStep", 5);
+ int delta = e->delta() > 0 ? step : -step;
+ int newVol = m_device->volume() + delta;
+ if ( newVol < 0 ) newVol = 0;
+ if ( newVol > 100 ) newVol = 100;
+ m_device->setVolume( newVol );
+}
+
+void TmixTray::contextMenuAboutToShow( TDEPopupMenu *menu )
+{
+ while ( menu->count() > 1 )
+ menu->removeItemAt( 1 );
+
+ TQWidget *win = parentWidget();
+ menu->insertItem( SmallIcon("configure"), i18n("&Configure TMix..."),
+ win, TQ_SLOT(showPreferences()) );
+ menu->insertItem( SmallIcon("help"), i18n("&About TMix"),
+ win, TQ_SLOT(showAbout()) );
+ menu->insertSeparator();
+ menu->insertItem( SmallIcon("application-exit"), i18n("&Quit"),
+ win, TQ_SLOT(quit()) );
+}
+
+#include "tmixtray.moc"
diff --git a/src/ui/tmixtray.h b/src/ui/tmixtray.h
new file mode 100644
index 0000000..08a5fb5
--- /dev/null
+++ b/src/ui/tmixtray.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <ksystemtray.h>
+
+class AudioDevice;
+class PulseModel;
+class TmixPopup;
+
+class TmixTray : public KSystemTray
+{
+ TQ_OBJECT
+
+public:
+ TmixTray( TQWidget *parent );
+ void setDevice( AudioDevice *dev );
+ void setModel( PulseModel *model );
+
+public slots:
+ void updateIcon();
+ void onRecordingActive( bool active );
+ void onDeviceAdded( AudioDevice *dev );
+
+protected:
+ void mousePressEvent( TQMouseEvent *e );
+ void wheelEvent( TQWheelEvent *e );
+ void contextMenuAboutToShow( TDEPopupMenu *menu );
+
+private:
+ void updateRecordingTray();
+
+ PulseModel *m_model;
+ AudioDevice *m_device;
+ KSystemTray *m_recTray;
+ TmixPopup *m_popup;
+ int m_recordingCount;
+};