diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-05-15 10:10:04 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-05-15 10:10:04 -0400 |
| commit | e776bc768cf9afca1867200e25d64d315cd72a3e (patch) | |
| tree | 6745527b939c9d37147d7dc98e8664437ee433f6 /src/ui/mixerwindow.cpp | |
| parent | 4e602e78cdfc210ab7781668df2a88afb923258b (diff) | |
Full mixer implementation — tray, popup, prefs, devices tab with port indicators
Adds the complete tmix feature set built since the initial skeleton:
balance knob, level meters, KLed mute button, system tray with scroll-wheel
volume and recording indicator, tray popup, preferences dialog, right-click
context menus, single-instance enforcement, scroll area, window geometry
persistence, and Devices tab with PA card profile switcher and live port
availability indicators (2-column layout, in-place updates on plug/unplug).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src/ui/mixerwindow.cpp')
| -rw-r--r-- | src/ui/mixerwindow.cpp | 381 |
1 files changed, 360 insertions, 21 deletions
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" |
