#include "devicewidget.h" #include "kledbutton.h" #include "levelmeter.h" #include "../model/audiodevice.h" #include "../model/pulsedevice.h" #include "../model/pulsemodel.h" #include #include #include #include #include #include #include #include "balanceknob.h" #include #include #include #include #include #include #include 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, PulseModel *model, TQWidget *parent ) : TQWidget(parent), m_device(device), m_model(model), m_sep(0), m_recordingLed(0), m_defaultBtn(0) { // ---- widgets ------------------------------------------------------------- m_label = new VerticalLabel( device->name(), this ); // 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_volLabel = new TQLabel( TQString::number(device->volume()) + "%", this ); m_volLabel->setAlignment( TQLabel::AlignHCenter ); m_levelMeter = new LevelMeter( this ); m_levelMeter->setFixedWidth( 14 ); m_volSlider = new TQSlider( 0, 100, 5, 100 - device->volume(), TQt::Vertical, this ); m_volSlider->setTickmarks( TQSlider::Right ); m_volSlider->setTickInterval( 5 ); // ---- layout -------------------------------------------------------------- // 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 ) { int pct = 100 - v; m_volLabel->setText( TQString::number(pct) + "%" ); m_device->setVolume( pct ); } 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 ) { 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_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 ) { static_cast(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 sinks = m_model->devices( AudioDevice::Output ); if ( !sinks.isEmpty() ) { menu.insertTitle( i18n("Move to Sink") ); m_moveTargets.clear(); PulseDevice *pd = dynamic_cast(m_device); uint32_t currentSink = pd ? pd->sinkIndex() : PA_INVALID_INDEX; int idx = 0; for ( TQPtrListIterator it(sinks); *it; ++it ) { m_moveTargets.append( *it ); int itemId = menu.insertItem( (*it)->name(), 1000 + idx ); PulseDevice *sink = dynamic_cast(*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"