summaryrefslogtreecommitdiff
path: root/src/ui/devicewidget.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/devicewidget.cpp')
-rw-r--r--src/ui/devicewidget.cpp384
1 files changed, 349 insertions, 35 deletions
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"