summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCalvin Morrison <calvin@pobox.com>2026-05-15 15:02:25 -0400
committerCalvin Morrison <calvin@pobox.com>2026-05-15 15:02:25 -0400
commite0c8fb0cdcb9c95e3efa60322c1733df0a965650 (patch)
treefac3f3dfd2b7d0c0e4e854387be91117088288af
parente776bc768cf9afca1867200e25d64d315cd72a3e (diff)
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 <noreply@anthropic.com>
-rw-r--r--CMakeLists.txt10
-rw-r--r--TODO5
-rw-r--r--src/model/pulsedevice.cpp1
-rw-r--r--src/model/pulsemodel.cpp6
-rw-r--r--src/ui/devicewidget.cpp18
-rw-r--r--src/ui/devicewidget.h1
-rw-r--r--src/ui/mixerwindow.cpp33
-rw-r--r--src/ui/mixerwindow.h3
-rw-r--r--src/ui/preferencesdlg.cpp62
-rw-r--r--src/ui/preferencesdlg.h9
-rw-r--r--src/ui/tmixpopup.cpp146
-rw-r--r--src/ui/tmixpopup.h33
-rw-r--r--src/ui/tmixtray.cpp104
-rw-r--r--src/ui/tmixtray.h11
14 files changed, 345 insertions, 97 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index de5ba5e..310c5ca 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -30,6 +30,16 @@ install(
FILES_MATCHING PATTERN "*.png"
)
+install(
+ FILES tmix.desktop
+ DESTINATION ${XDG_APPS_INSTALL_DIR}
+)
+
+install(
+ FILES img/tmix.png
+ DESTINATION ${DATA_INSTALL_DIR}/icons/hicolor/22x22/apps
+)
+
tde_add_executable( tmix AUTOMOC
SOURCES
src/main.cpp
diff --git a/TODO b/TODO
index 9834c25..dd0e902 100644
--- a/TODO
+++ b/TODO
@@ -112,7 +112,7 @@ Run through this before any release / major refactor.
- [x] Window does not resize on card profile change (removed adjustSize())
- [x] Window position persists across restarts
- [x] Single-instance enforcement — second launch raises/shows the existing window (KUniqueApplication)
-- [x] Settings dialog is non-blocking (modeless show(), reloads config on each open)
+- [x] Settings dialog is non-blocking (modeless, false modal param in KDialogBase ctor)
- [ ] Devices tab — sex it up vs pavucontrol-qt:
- Port availability indicators (plugged/unplugged) with port-type icons
(headphones, mic, HDMI, speaker, S/PDIF) from pa_card_port_info.type
@@ -191,10 +191,7 @@ Architecture:
Why: the OSS→ALSA→PA retrofit history was painful. No backend should ever know
about another; no generic UI code should check backend capabilities via flags.
-- [ ] L/R channel lock/unlock button — when unlocked, show two independent sliders
- for left and right channels; when locked, they move together (current behavior)
- [ ] DCOP interface (get/set volume, mute, list devices)
-- [ ] Dark/light theme awareness (follow TDE palette)
- [ ] Horizontal layout mode — device strips laid out left-to-right instead of
top-to-bottom; label above slider, level meter beside it. Toggle in View menu.
- [ ] Current mixer selector — dropdown or sub-menu to switch between PulseAudio
diff --git a/src/model/pulsedevice.cpp b/src/model/pulsedevice.cpp
index de778b9..8bdea54 100644
--- a/src/model/pulsedevice.cpp
+++ b/src/model/pulsedevice.cpp
@@ -110,6 +110,7 @@ void PulseDevice::startMonitoring()
pa_stream_set_read_callback( m_monitorStream, monitorReadCb, this );
// For sink inputs, filter peak detection to just this stream.
+ // Source outputs don't support pa_stream_set_monitor_stream; they connect to the source directly.
if ( m_category == Playback )
pa_stream_set_monitor_stream( m_monitorStream, m_paIndex );
diff --git a/src/model/pulsemodel.cpp b/src/model/pulsemodel.cpp
index a677edc..640b918 100644
--- a/src/model/pulsemodel.cpp
+++ b/src/model/pulsemodel.cpp
@@ -596,7 +596,11 @@ void PulseModel::customEvent( TQCustomEvent *e )
if ( ev->cat == AudioDevice::Recording && ev->parentIndex != PA_INVALID_INDEX ) {
m_sourceOutputToSource.insert( ev->paIndex, ev->parentIndex );
PulseDevice *src = findDevice( m_sources, ev->parentIndex );
- if ( src ) src->adjustRecordingCount( +1 );
+ if ( src ) {
+ src->adjustRecordingCount( +1 );
+ connect( src, TQ_SIGNAL(levelChanged(float)),
+ dev, TQ_SIGNAL(levelChanged(float)) );
+ }
}
}
}
diff --git a/src/ui/devicewidget.cpp b/src/ui/devicewidget.cpp
index e6401d2..22e2995 100644
--- a/src/ui/devicewidget.cpp
+++ b/src/ui/devicewidget.cpp
@@ -17,6 +17,7 @@
#include <kled.h>
#include <kiconloader.h>
#include <tdeglobal.h>
+#include <tdeconfig.h>
#include <kstandarddirs.h>
#include <tdepopupmenu.h>
#include <tdelocale.h>
@@ -287,6 +288,17 @@ void DeviceWidget::onDeviceName( const TQString &name )
static_cast<VerticalLabel*>(m_label)->setText( name );
}
+void DeviceWidget::wheelEvent( TQWheelEvent *e )
+{
+ TDEConfig *cfg = TDEGlobal::config();
+ cfg->setGroup("General");
+ int step = cfg->readNumEntry("ScrollStep", 5);
+ int delta = e->delta() > 0 ? step : -step;
+ int vol = TQMAX( 0, TQMIN( 100, m_device->volume() + delta ) );
+ m_device->setVolume( vol );
+ e->accept();
+}
+
void DeviceWidget::contextMenuEvent( TQContextMenuEvent *e )
{
TDEPopupMenu menu( this );
@@ -300,11 +312,13 @@ void DeviceWidget::contextMenuEvent( TQContextMenuEvent *e )
if ( cat == AudioDevice::Output ) {
menu.insertSeparator();
- menu.insertItem( i18n("Set as Default Output"), this, TQ_SLOT(onSetDefault()) );
+ int id = menu.insertItem( i18n("Set as Default Output"), this, TQ_SLOT(onSetDefault()) );
+ menu.setItemChecked( id, m_model->defaultOutput() == m_device );
} else if ( cat == AudioDevice::Input ) {
menu.insertSeparator();
- menu.insertItem( i18n("Set as Default Input"), this, TQ_SLOT(onSetDefault()) );
+ int id = menu.insertItem( i18n("Set as Default Input"), this, TQ_SLOT(onSetDefault()) );
+ menu.setItemChecked( id, m_model->defaultInput() == m_device );
} else if ( cat == AudioDevice::Playback ) {
TQPtrList<AudioDevice> sinks = m_model->devices( AudioDevice::Output );
diff --git a/src/ui/devicewidget.h b/src/ui/devicewidget.h
index a5532bf..802014e 100644
--- a/src/ui/devicewidget.h
+++ b/src/ui/devicewidget.h
@@ -31,6 +31,7 @@ public:
protected:
void contextMenuEvent( TQContextMenuEvent *e );
+ void wheelEvent( TQWheelEvent *e );
private slots:
void onVolumeChanged( int v );
diff --git a/src/ui/mixerwindow.cpp b/src/ui/mixerwindow.cpp
index caa6af5..53113e6 100644
--- a/src/ui/mixerwindow.cpp
+++ b/src/ui/mixerwindow.cpp
@@ -1,5 +1,6 @@
#include "mixerwindow.h"
#include "preferencesdlg.h"
+#include "devicewidget.h"
#include "../model/pulsemodel.h"
#include <tqlayout.h>
@@ -20,6 +21,7 @@
#include <tdeaboutapplication.h>
#include <tdeaboutdata.h>
#include <tdeapplication.h>
+#include <twin.h>
#include "tmixtray.h"
#include "devicespage.h"
#include <tdeconfig.h>
@@ -202,6 +204,12 @@ bool MixerWindow::queryClose()
}
+void MixerWindow::bringToFront()
+{
+ show();
+ KWin::forceActiveWindow( winId() );
+}
+
void MixerWindow::showAbout()
{
TDEAboutData about(
@@ -224,6 +232,21 @@ MixerWindow::Tab &MixerWindow::tabForCategory( AudioDevice::Category cat )
return m_output;
}
+static void updateSeparators( TQHBoxLayout *layout )
+{
+ TQLayoutIterator it = layout->iterator();
+ DeviceWidget *last = 0;
+ while ( TQLayoutItem *item = it.current() ) {
+ DeviceWidget *dw = dynamic_cast<DeviceWidget*>( item->widget() );
+ if ( dw ) {
+ if ( last ) last->setSeparatorVisible( true );
+ last = dw;
+ }
+ ++it;
+ }
+ if ( last ) last->setSeparatorVisible( false );
+}
+
void MixerWindow::onDeviceAdded( AudioDevice *dev )
{
if ( m_stack->id( m_stack->visibleWidget() ) == 1 ) {
@@ -232,6 +255,7 @@ void MixerWindow::onDeviceAdded( AudioDevice *dev )
m_allCount++;
w->show();
m_widgets.insert( dev, w );
+ updateSeparators( m_allLayout );
} else {
Tab &t = tabForCategory( dev->category() );
TQWidget *w = dev->createWidget( t.page );
@@ -239,6 +263,7 @@ void MixerWindow::onDeviceAdded( AudioDevice *dev )
t.count++;
w->show();
m_widgets.insert( dev, w );
+ updateSeparators( t.layout );
}
}
@@ -255,13 +280,17 @@ void MixerWindow::onDeviceRemoved( AudioDevice *dev )
if ( m_stack->id( m_stack->visibleWidget() ) == 1 ) {
m_allLayout->remove( w );
m_allCount--;
+ m_widgets.remove( dev );
+ delete w;
+ updateSeparators( m_allLayout );
} else {
Tab &t = tabForCategory( dev->category() );
t.layout->remove( w );
t.count--;
+ m_widgets.remove( dev );
+ delete w;
+ updateSeparators( t.layout );
}
- m_widgets.remove( dev );
- delete w;
}
diff --git a/src/ui/mixerwindow.h b/src/ui/mixerwindow.h
index c472246..fa613cd 100644
--- a/src/ui/mixerwindow.h
+++ b/src/ui/mixerwindow.h
@@ -33,6 +33,9 @@ protected:
bool eventFilter( TQObject *obj, TQEvent *e );
void showEvent( TQShowEvent *e );
+public slots:
+ void bringToFront();
+
private slots:
void onDeviceAdded( AudioDevice *dev );
void onDeviceRemoved( AudioDevice *dev );
diff --git a/src/ui/preferencesdlg.cpp b/src/ui/preferencesdlg.cpp
index b6df120..b263850 100644
--- a/src/ui/preferencesdlg.cpp
+++ b/src/ui/preferencesdlg.cpp
@@ -2,16 +2,18 @@
#include <tqlayout.h>
#include <tqcheckbox.h>
+#include <tqcombobox.h>
#include <tqspinbox.h>
#include <tqlabel.h>
#include <tqframe.h>
+#include <tqgroupbox.h>
#include <tdeglobal.h>
#include <tdeconfig.h>
#include <tdelocale.h>
PreferencesDlg::PreferencesDlg( TQWidget *parent )
: KDialogBase( Tabbed, i18n("Configure TMix"),
- Ok | Apply | Cancel, Ok, parent )
+ Ok | Apply | Cancel, Ok, parent, 0, false )
{
// ---- General -------------------------------------------------------------
TQFrame *gen = addPage( i18n("General") );
@@ -20,8 +22,33 @@ PreferencesDlg::PreferencesDlg( TQWidget *parent )
m_dockInTray = new TQCheckBox( i18n("Dock in system tray"), gen );
gl->addWidget( m_dockInTray );
- m_showPopup = new TQCheckBox( i18n("Show mini volume popup on tray click"), gen );
- gl->addWidget( m_showPopup );
+ gl->addSpacing( 4 );
+
+ TQHBoxLayout *popupRow = new TQHBoxLayout( 0, 0, spacingHint() );
+ popupRow->addWidget( new TQLabel( i18n("Tray left-click:"), gen ) );
+ m_popupMode = new TQComboBox( false, gen );
+ m_popupMode->insertItem( i18n("Show mixer window") );
+ m_popupMode->insertItem( i18n("Show mini popup") );
+ popupRow->addWidget( m_popupMode );
+ popupRow->addStretch();
+ gl->addLayout( popupRow );
+
+ TQGroupBox *popupContent = new TQGroupBox( i18n("Mini popup shows:"), gen );
+ popupContent->setColumnLayout( 0, Qt::Vertical );
+ popupContent->layout()->setSpacing( spacingHint() );
+ popupContent->layout()->setMargin( marginHint() );
+ TQVBoxLayout *pcl = new TQVBoxLayout( popupContent->layout() );
+ m_popupShowOutput = new TQCheckBox( i18n("Default output (speakers/headphones)"), popupContent );
+ m_popupShowMic = new TQCheckBox( i18n("Microphone inputs"), popupContent );
+ m_popupShowApps = new TQCheckBox( i18n("Active app streams"), popupContent );
+ pcl->addWidget( m_popupShowOutput );
+ pcl->addWidget( m_popupShowMic );
+ pcl->addWidget( m_popupShowApps );
+ gl->addWidget( popupContent );
+
+ connect( m_popupMode, TQ_SIGNAL(activated(int)), this, TQ_SLOT(onPopupModeChanged(int)) );
+
+ gl->addSpacing( 4 );
m_showRecTray = new TQCheckBox( i18n("Show microphone-in-use icon in tray"), gen );
gl->addWidget( m_showRecTray );
@@ -29,7 +56,6 @@ PreferencesDlg::PreferencesDlg( TQWidget *parent )
m_confirmQuit = new TQCheckBox( i18n("Ask for confirmation before quitting"), gen );
gl->addWidget( m_confirmQuit );
-
gl->addSpacing( 8 );
TQHBoxLayout *stepRow = new TQHBoxLayout( 0, 0, spacingHint() );
@@ -64,13 +90,26 @@ PreferencesDlg::PreferencesDlg( TQWidget *parent )
load();
}
+void PreferencesDlg::onPopupModeChanged( int idx )
+{
+ bool popupEnabled = ( idx == 1 );
+ m_popupShowOutput->setEnabled( popupEnabled );
+ m_popupShowMic->setEnabled( popupEnabled );
+ m_popupShowApps->setEnabled( popupEnabled );
+}
+
void PreferencesDlg::load()
{
TDEConfig *cfg = TDEGlobal::config();
cfg->setGroup("General");
m_dockInTray->setChecked( cfg->readBoolEntry("DockInTray", true) );
- m_showPopup->setChecked( cfg->readBoolEntry("ShowPopup", true) );
+ int mode = cfg->readNumEntry("PopupMode", 1);
+ m_popupMode->setCurrentItem( mode );
+ m_popupShowOutput->setChecked( cfg->readBoolEntry("PopupShowOutput", true) );
+ m_popupShowMic->setChecked( cfg->readBoolEntry("PopupShowMic", false) );
+ m_popupShowApps->setChecked( cfg->readBoolEntry("PopupShowApps", true) );
+ onPopupModeChanged( mode );
m_showRecTray->setChecked( cfg->readBoolEntry("ShowRecordingTray", true) );
m_confirmQuit->setChecked( cfg->readBoolEntry("ConfirmQuit", false) );
m_scrollStep->setValue( cfg->readNumEntry( "ScrollStep", 5) );
@@ -88,11 +127,14 @@ void PreferencesDlg::save()
TDEConfig *cfg = TDEGlobal::config();
cfg->setGroup("General");
- cfg->writeEntry( "DockInTray", m_dockInTray->isChecked() );
- cfg->writeEntry( "ShowPopup", m_showPopup->isChecked() );
- cfg->writeEntry( "ShowRecordingTray", m_showRecTray->isChecked() );
- cfg->writeEntry( "ConfirmQuit", m_confirmQuit->isChecked() );
- cfg->writeEntry( "ScrollStep", m_scrollStep->value() );
+ cfg->writeEntry( "DockInTray", m_dockInTray->isChecked() );
+ cfg->writeEntry( "PopupMode", m_popupMode->currentItem() );
+ cfg->writeEntry( "PopupShowOutput", m_popupShowOutput->isChecked() );
+ cfg->writeEntry( "PopupShowMic", m_popupShowMic->isChecked() );
+ cfg->writeEntry( "PopupShowApps", m_popupShowApps->isChecked() );
+ cfg->writeEntry( "ShowRecordingTray", m_showRecTray->isChecked() );
+ cfg->writeEntry( "ConfirmQuit", m_confirmQuit->isChecked() );
+ cfg->writeEntry( "ScrollStep", m_scrollStep->value() );
cfg->setGroup("View");
cfg->writeEntry( "NoTabs", m_noTabs->isChecked() );
diff --git a/src/ui/preferencesdlg.h b/src/ui/preferencesdlg.h
index 94f5682..09d740f 100644
--- a/src/ui/preferencesdlg.h
+++ b/src/ui/preferencesdlg.h
@@ -3,6 +3,7 @@
#include <kdialogbase.h>
class TQCheckBox;
+class TQComboBox;
class TQSpinBox;
class PreferencesDlg : public KDialogBase
@@ -20,11 +21,17 @@ protected slots:
void slotOk();
void slotApply();
+private slots:
+ void onPopupModeChanged( int idx );
+
private:
void save();
TQCheckBox *m_dockInTray;
- TQCheckBox *m_showPopup;
+ TQComboBox *m_popupMode;
+ TQCheckBox *m_popupShowOutput;
+ TQCheckBox *m_popupShowMic;
+ TQCheckBox *m_popupShowApps;
TQCheckBox *m_showRecTray;
TQCheckBox *m_confirmQuit;
TQSpinBox *m_scrollStep;
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 <tqlayout.h>
#include <tqframe.h>
-#include <tqlabel.h>
-#include <tqpushbutton.h>
#include <tqapplication.h>
#include <tqdesktopwidget.h>
#include <tdelocale.h>
@@ -14,69 +12,127 @@
#include <tdeglobal.h>
#include <twin.h>
-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<TQWidget*>( m_layout->parent() ) );
- m_devWidget->setSeparatorVisible( false );
- m_layout->insertWidget( 2, m_devWidget );
- m_devWidget->show();
+ if ( m_showMic ) {
+ TQPtrList<AudioDevice> inputs = m_model->devices( AudioDevice::Input );
+ if ( !inputs.isEmpty() ) {
+ addSep();
+ for ( TQPtrListIterator<AudioDevice> it(inputs); *it; ++it )
+ addWidget( *it );
+ }
+ }
+
+ if ( m_showApps ) {
+ TQPtrList<AudioDevice> apps = m_model->devices( AudioDevice::Playback );
+ if ( !apps.isEmpty() ) {
+ addSep();
+ for ( TQPtrListIterator<AudioDevice> it(apps); *it; ++it )
+ addWidget( *it );
+ }
+ }
+
+ if ( m_showRecording ) {
+ TQPtrList<AudioDevice> recs = m_model->devices( AudioDevice::Recording );
+ if ( !recs.isEmpty() ) {
+ addSep();
+ for ( TQPtrListIterator<AudioDevice> 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();
diff --git a/src/ui/tmixpopup.h b/src/ui/tmixpopup.h
index 4dbdc9a..527f7f7 100644
--- a/src/ui/tmixpopup.h
+++ b/src/ui/tmixpopup.h
@@ -2,41 +2,46 @@
#include <tqwidget.h>
#include <tqdatetime.h>
+#include <tqptrlist.h>
class PulseModel;
class AudioDevice;
class DeviceWidget;
+class TQHBoxLayout;
class TQVBoxLayout;
-class TQPushButton;
class TmixPopup : public TQWidget
{
TQ_OBJECT
public:
- explicit TmixPopup( PulseModel *model, TQWidget *parent = 0 );
+ TmixPopup( PulseModel *model,
+ bool showOutput, bool showMic, bool showApps,
+ bool showRecording = false,
+ TQWidget *parent = 0 );
- // Position relative to tray icon and show, or hide if already visible.
void toggleAt( const TQPoint &trayGlobalPos, const TQSize &traySize );
-
- // True for ~300ms after hiding — prevents re-show on the same click.
bool justHidden() const;
-signals:
- void showMixerRequested();
-
protected:
void hideEvent( TQHideEvent *e );
void wheelEvent( TQWheelEvent *e );
private slots:
void onDefaultOutputChanged( AudioDevice *dev );
+ void onDeviceAdded( AudioDevice *dev );
+ void onDeviceRemoved( AudioDevice *dev );
private:
- void setDevice( AudioDevice *dev );
-
- PulseModel *m_model;
- DeviceWidget *m_devWidget;
- TQVBoxLayout *m_layout;
- TQTime m_hideTime;
+ void rebuild();
+
+ PulseModel *m_model;
+ bool m_showOutput;
+ bool m_showMic;
+ bool m_showApps;
+ bool m_showRecording;
+ TQVBoxLayout *m_outerLayout;
+ TQWidget *m_devContainer;
+ TQPtrList<DeviceWidget> m_devWidgets;
+ TQTime m_hideTime;
};
diff --git a/src/ui/tmixtray.cpp b/src/ui/tmixtray.cpp
index c0980f0..4048270 100644
--- a/src/ui/tmixtray.cpp
+++ b/src/ui/tmixtray.cpp
@@ -1,5 +1,24 @@
#include "tmixtray.h"
#include "tmixpopup.h"
+#include "mixerwindow.h"
+
+class RecordingTray : public KSystemTray
+{
+public:
+ RecordingTray( TmixTray *owner, TQWidget *parent )
+ : KSystemTray(parent), m_owner(owner) {}
+protected:
+ void mousePressEvent( TQMouseEvent *e ) {
+ if ( e->button() == TQt::LeftButton ) {
+ m_owner->toggleRecordingPopup();
+ e->accept();
+ } else {
+ KSystemTray::mousePressEvent( e );
+ }
+ }
+private:
+ TmixTray *m_owner;
+};
#include "../model/audiodevice.h"
#include "../model/pulsemodel.h"
@@ -7,6 +26,7 @@
#include <tqpixmap.h>
#include <tqstringlist.h>
#include <tqtooltip.h>
+#include <tqapplication.h>
#include <tdeglobal.h>
#include <kiconloader.h>
#include <kstandarddirs.h>
@@ -16,7 +36,9 @@
TmixTray::TmixTray( TQWidget *parent )
: KSystemTray(parent), m_model(0), m_device(0), m_recTray(0),
- m_popup(0), m_recordingCount(0)
+ m_popup(0), m_recPopup(0), m_popupMode(-1),
+ m_popupShowOutput(false), m_popupShowMic(false), m_popupShowApps(false),
+ m_recordingCount(0)
{
setCaption( i18n("Volume Control") );
@@ -58,29 +80,49 @@ void TmixTray::mousePressEvent( TQMouseEvent *e )
{
if ( e->button() == TQt::LeftButton ) {
if ( !m_model ) { KSystemTray::mousePressEvent(e); return; }
-
- TDEConfig *cfg = TDEGlobal::config();
- cfg->setGroup("General");
- bool showPopup = cfg->readBoolEntry("ShowPopup", true);
-
- if ( showPopup ) {
- if ( !m_popup ) {
- m_popup = new TmixPopup( m_model, this );
- connect( m_popup, TQ_SIGNAL(showMixerRequested()),
- parentWidget(), TQ_SLOT(show()) );
- }
- if ( m_popup->justHidden() ) { e->accept(); return; }
- m_popup->toggleAt( mapToGlobal( TQPoint(0,0) ), size() );
- } else {
- TQWidget *win = parentWidget();
- win->isVisible() ? win->hide() : win->show();
- }
+ executeSingleClick();
e->accept();
return;
}
KSystemTray::mousePressEvent( e );
}
+
+void TmixTray::executeSingleClick()
+{
+ TDEConfig *cfg = TDEGlobal::config();
+ cfg->setGroup("General");
+ int mode = cfg->readNumEntry( "PopupMode", 1 );
+ bool showOutput = cfg->readBoolEntry("PopupShowOutput", true );
+ bool showMic = cfg->readBoolEntry("PopupShowMic", false );
+ bool showApps = cfg->readBoolEntry("PopupShowApps", true );
+
+ if ( mode >= 1 ) {
+ if ( m_popup && ( mode != m_popupMode ||
+ showOutput != m_popupShowOutput ||
+ showMic != m_popupShowMic ||
+ showApps != m_popupShowApps ) ) {
+ delete m_popup;
+ m_popup = 0;
+ }
+ if ( !m_popup ) {
+ m_popupMode = mode;
+ m_popupShowOutput = showOutput;
+ m_popupShowMic = showMic;
+ m_popupShowApps = showApps;
+ m_popup = new TmixPopup( m_model, showOutput, showMic, showApps, this );
+ }
+ if ( m_popup->justHidden() ) return;
+ m_popup->toggleAt( mapToGlobal( TQPoint(0,0) ), size() );
+ } else {
+ MixerWindow *win = static_cast<MixerWindow*>( parentWidget() );
+ if ( win->isVisible() )
+ win->hide();
+ else
+ win->bringToFront();
+ }
+}
+
void TmixTray::updateIcon()
{
int vol = m_device ? m_device->volume() : 0;
@@ -120,7 +162,7 @@ void TmixTray::updateRecordingTray()
if ( m_recordingCount > 0 && showRec ) {
if ( !m_recTray ) {
- m_recTray = new KSystemTray( parentWidget() );
+ m_recTray = new RecordingTray( this, parentWidget() );
m_recTray->setCaption( i18n("Microphone Active") );
TQString path = TDEGlobal::dirs()->findResource( "data",
@@ -148,9 +190,24 @@ void TmixTray::updateRecordingTray()
m_recTray->show();
} else if ( m_recTray ) {
m_recTray->hide();
+ if ( m_recPopup && m_recPopup->isVisible() )
+ m_recPopup->hide();
}
}
+void TmixTray::toggleRecordingPopup()
+{
+ if ( !m_model ) return;
+ if ( !m_recPopup )
+ m_recPopup = new TmixPopup( m_model,
+ false /*output*/, true /*mic*/,
+ false /*apps*/, true /*recording*/,
+ 0 );
+ if ( m_recPopup->justHidden() ) return;
+ TQPoint pos = m_recTray->mapToGlobal( TQPoint(0,0) );
+ m_recPopup->toggleAt( pos, m_recTray->size() );
+}
+
void TmixTray::wheelEvent( TQWheelEvent *e )
{
if ( !m_device ) return;
@@ -169,7 +226,14 @@ void TmixTray::contextMenuAboutToShow( TDEPopupMenu *menu )
while ( menu->count() > 1 )
menu->removeItemAt( 1 );
- TQWidget *win = parentWidget();
+ MixerWindow *win = static_cast<MixerWindow*>( parentWidget() );
+
+ TDEConfig *cfg = TDEGlobal::config();
+ cfg->setGroup("General");
+ if ( cfg->readNumEntry("PopupMode", 1) >= 1 )
+ menu->insertItem( SmallIcon("tmix"), i18n("&Open Mixer"),
+ win, TQ_SLOT(bringToFront()) );
+
menu->insertItem( SmallIcon("configure"), i18n("&Configure TMix..."),
win, TQ_SLOT(showPreferences()) );
menu->insertItem( SmallIcon("help"), i18n("&About TMix"),
diff --git a/src/ui/tmixtray.h b/src/ui/tmixtray.h
index 08a5fb5..f04450c 100644
--- a/src/ui/tmixtray.h
+++ b/src/ui/tmixtray.h
@@ -25,6 +25,12 @@ protected:
void wheelEvent( TQWheelEvent *e );
void contextMenuAboutToShow( TDEPopupMenu *menu );
+public:
+ void toggleRecordingPopup();
+
+private slots:
+ void executeSingleClick();
+
private:
void updateRecordingTray();
@@ -32,5 +38,10 @@ private:
AudioDevice *m_device;
KSystemTray *m_recTray;
TmixPopup *m_popup;
+ TmixPopup *m_recPopup;
+ int m_popupMode;
+ bool m_popupShowOutput;
+ bool m_popupShowMic;
+ bool m_popupShowApps;
int m_recordingCount;
};