diff options
Diffstat (limited to 'src/ui/devicewidget.cpp')
| -rw-r--r-- | src/ui/devicewidget.cpp | 384 |
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" |
