From e0c8fb0cdcb9c95e3efa60322c1733df0a965650 Mon Sep 17 00:00:00 2001 From: Calvin Morrison Date: Fri, 15 May 2026 15:02:25 -0400 Subject: Recording popup, level meters, UX polish - Recording tray icon opens popup (mics + active recording streams) - Recording stream level meters forward from parent source signal - RecordingTray subclass for single-click (no double-click needed) - Context menu Set Default Output/Input shows checkmark when active - Last DeviceWidget in each row hides its right separator - Popup horizontal layout, configurable content (output/mic/apps) - Single-click tray, right-click menu for Open Mixer - Desktop file, icon, CMake install rules - Window bring-to-front across workspaces (KWin::forceActiveWindow) Co-Authored-By: Claude Sonnet 4.6 --- src/ui/tmixpopup.cpp | 146 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 103 insertions(+), 43 deletions(-) (limited to 'src/ui/tmixpopup.cpp') diff --git a/src/ui/tmixpopup.cpp b/src/ui/tmixpopup.cpp index 068c66e..85a0c54 100644 --- a/src/ui/tmixpopup.cpp +++ b/src/ui/tmixpopup.cpp @@ -5,8 +5,6 @@ #include #include -#include -#include #include #include #include @@ -14,69 +12,127 @@ #include #include -TmixPopup::TmixPopup( PulseModel *model, TQWidget *parent ) +TmixPopup::TmixPopup( PulseModel *model, + bool showOutput, bool showMic, bool showApps, + bool showRecording, + TQWidget *parent ) : TQWidget( parent, "TmixPopup", WStyle_Customize | WType_Popup | WStyle_DialogBorder ), - m_model(model), m_devWidget(0) + m_model(model), m_showOutput(showOutput), + m_showMic(showMic), m_showApps(showApps), m_showRecording(showRecording), + m_devContainer(0) { - // Single frame fills the popup — header and content share the same border + m_devWidgets.setAutoDelete( true ); + 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 ); + m_outerLayout = new TQVBoxLayout( frame, 0, 0 ); - // device widget inserted at index 0 by setDevice() + m_devContainer = new TQWidget( frame ); + m_outerLayout->addWidget( m_devContainer ); - 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()) ); + m_outerLayout->addSpacing( 4 ); connect( model, TQ_SIGNAL(defaultOutputChanged(AudioDevice*)), this, TQ_SLOT(onDefaultOutputChanged(AudioDevice*)) ); + connect( model, TQ_SIGNAL(deviceAdded(AudioDevice*)), + this, TQ_SLOT(onDeviceAdded(AudioDevice*)) ); + connect( model, TQ_SIGNAL(deviceRemoved(AudioDevice*)), + this, TQ_SLOT(onDeviceRemoved(AudioDevice*)) ); - setDevice( model->defaultOutput() ); + rebuild(); } -void TmixPopup::setDevice( AudioDevice *dev ) +void TmixPopup::rebuild() { - if ( m_devWidget ) { - m_layout->remove( m_devWidget ); - delete m_devWidget; - m_devWidget = 0; + m_devWidgets.clear(); + delete m_devContainer->layout(); + + TQHBoxLayout *hbox = new TQHBoxLayout( m_devContainer, 4, 0 ); + bool anyAdded = false; + + auto addSep = [&]() { + if ( !anyAdded ) return; + TQFrame *sep = new TQFrame( m_devContainer ); + sep->setFrameStyle( TQFrame::VLine | TQFrame::Sunken ); + sep->setFixedWidth( 4 ); + hbox->addWidget( sep ); + }; + + auto addWidget = [&]( AudioDevice *dev ) { + DeviceWidget *w = new DeviceWidget( dev, m_model, m_devContainer ); + w->setSeparatorVisible( false ); + hbox->addWidget( w ); + w->show(); + m_devWidgets.append( w ); + anyAdded = true; + }; + + if ( m_showOutput ) { + AudioDevice *def = m_model->defaultOutput(); + if ( def ) { + addSep(); + addWidget( def ); + } } - if ( !dev ) return; - m_devWidget = new DeviceWidget( dev, m_model, - static_cast( m_layout->parent() ) ); - m_devWidget->setSeparatorVisible( false ); - m_layout->insertWidget( 2, m_devWidget ); - m_devWidget->show(); + if ( m_showMic ) { + TQPtrList inputs = m_model->devices( AudioDevice::Input ); + if ( !inputs.isEmpty() ) { + addSep(); + for ( TQPtrListIterator it(inputs); *it; ++it ) + addWidget( *it ); + } + } + + if ( m_showApps ) { + TQPtrList apps = m_model->devices( AudioDevice::Playback ); + if ( !apps.isEmpty() ) { + addSep(); + for ( TQPtrListIterator it(apps); *it; ++it ) + addWidget( *it ); + } + } + + if ( m_showRecording ) { + TQPtrList recs = m_model->devices( AudioDevice::Recording ); + if ( !recs.isEmpty() ) { + addSep(); + for ( TQPtrListIterator it(recs); *it; ++it ) + addWidget( *it ); + } + } + + m_devContainer->show(); adjustSize(); } -void TmixPopup::onDefaultOutputChanged( AudioDevice *dev ) +void TmixPopup::onDefaultOutputChanged( AudioDevice * ) { - setDevice( dev ); + if ( m_showOutput ) + rebuild(); +} + +void TmixPopup::onDeviceAdded( AudioDevice *dev ) +{ + AudioDevice::Category cat = dev->category(); + if ( ( m_showMic && cat == AudioDevice::Input ) || + ( m_showApps && cat == AudioDevice::Playback ) || + ( m_showRecording && cat == AudioDevice::Recording ) ) + rebuild(); +} + +void TmixPopup::onDeviceRemoved( AudioDevice *dev ) +{ + AudioDevice::Category cat = dev->category(); + if ( ( m_showMic && cat == AudioDevice::Input ) || + ( m_showApps && cat == AudioDevice::Playback ) || + ( m_showRecording && cat == AudioDevice::Recording ) ) + rebuild(); } void TmixPopup::toggleAt( const TQPoint &trayPos, const TQSize &traySize ) @@ -86,6 +142,7 @@ void TmixPopup::toggleAt( const TQPoint &trayPos, const TQSize &traySize ) return; } + rebuild(); adjustSize(); TQRect screen = TQApplication::desktop()->screenGeometry( trayPos ); @@ -95,7 +152,6 @@ void TmixPopup::toggleAt( const TQPoint &trayPos, const TQSize &traySize ) if ( y < screen.top() ) y = trayPos.y() + traySize.height(); - if ( x + width() > screen.right() ) x = screen.right() - width(); if ( x < screen.left() ) @@ -110,7 +166,7 @@ void TmixPopup::toggleAt( const TQPoint &trayPos, const TQSize &traySize ) bool TmixPopup::justHidden() const { - return m_hideTime.isValid() && m_hideTime.elapsed() < 300; + return m_hideTime.isValid() && m_hideTime.elapsed() < 100; } void TmixPopup::hideEvent( TQHideEvent *e ) @@ -121,6 +177,10 @@ void TmixPopup::hideEvent( TQHideEvent *e ) void TmixPopup::wheelEvent( TQWheelEvent *e ) { + // Only intercept scroll in single-output mode — with multiple devices + // visible, scroll should go to the device widget under the cursor. + if ( m_showMic || m_showApps || m_showRecording ) { e->ignore(); return; } + AudioDevice *dev = m_model->defaultOutput(); if ( !dev ) return; TDEConfig *cfg = TDEGlobal::config(); -- cgit v1.2.3