From 674308695242af0828190887f81baed48abb40ff Mon Sep 17 00:00:00 2001 From: Ivailo Monev Date: Wed, 20 Sep 2023 18:17:36 +0300 Subject: [PATCH] plasma: new mixer applet kmix replacement, most importantly - multi-channel! Signed-off-by: Ivailo Monev --- CMakeLists.txt | 7 + plasma/applets/CMakeLists.txt | 4 + plasma/applets/mixer/CMakeLists.txt | 23 + plasma/applets/mixer/mixer.cpp | 650 ++++++++++++++++++ plasma/applets/mixer/mixer.h | 48 ++ .../applets/mixer/plasma-applet-mixer.desktop | 20 + plasma/applets/systemtray/ui/applet.cpp | 4 + 7 files changed, 756 insertions(+) create mode 100644 plasma/applets/mixer/CMakeLists.txt create mode 100644 plasma/applets/mixer/mixer.cpp create mode 100644 plasma/applets/mixer/mixer.h create mode 100644 plasma/applets/mixer/plasma-applet-mixer.desktop diff --git a/CMakeLists.txt b/CMakeLists.txt index df94cba5..744129e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -204,6 +204,13 @@ set_package_properties(LibGit2 PROPERTIES PURPOSE "Needed for the Dolphin Git plugin" ) +kde4_optional_find_package(ALSA) +set_package_properties(ALSA PROPERTIES + DESCRIPTION "Advanced Linux Sound Architecture" + URL "https://alsa-project.org/wiki/Main_Page" + PURPOSE "Needed for Plasma mixer applet" +) + find_program(WRESTOOL_EXECUTABLE wrestool) add_feature_info(wrestool WRESTOOL_EXECUTABLE diff --git a/plasma/applets/CMakeLists.txt b/plasma/applets/CMakeLists.txt index accca0ba..b2691d65 100644 --- a/plasma/applets/CMakeLists.txt +++ b/plasma/applets/CMakeLists.txt @@ -19,3 +19,7 @@ add_subdirectory(folderview) add_subdirectory(tasks) add_subdirectory(windowlist) add_subdirectory(pager) + +if (ALSA_FOUND) + add_subdirectory(mixer) +endif() diff --git a/plasma/applets/mixer/CMakeLists.txt b/plasma/applets/mixer/CMakeLists.txt new file mode 100644 index 00000000..54e1a314 --- /dev/null +++ b/plasma/applets/mixer/CMakeLists.txt @@ -0,0 +1,23 @@ +project(plasma-applet-mixer) + +include_directories(${ALSA_INCLUDE_DIR}) + +set(mixer_SRCS + mixer.cpp +) + +kde4_add_plugin(plasma_applet_mixer ${mixer_SRCS}) +target_link_libraries(plasma_applet_mixer + KDE4::plasma + ${ALSA_LIBRARIES} +) + +install( + TARGETS plasma_applet_mixer + DESTINATION ${KDE4_PLUGIN_INSTALL_DIR} +) + +install( + FILES plasma-applet-mixer.desktop + DESTINATION ${KDE4_SERVICES_INSTALL_DIR} +) diff --git a/plasma/applets/mixer/mixer.cpp b/plasma/applets/mixer/mixer.cpp new file mode 100644 index 00000000..a991466c --- /dev/null +++ b/plasma/applets/mixer/mixer.cpp @@ -0,0 +1,650 @@ +/* This file is part of the KDE project + Copyright (C) 2023 Ivailo Monev + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2, as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "mixer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static const QSizeF s_minimumsize = QSizeF(290, 140); +static const QSizeF s_minimumslidersize = QSizeF(10, 70); +static const int s_svgiconsize = 256; +static const QString s_defaultpopupicon = QString::fromLatin1("audio-card"); + +static QList kALSAChannelTypes(snd_mixer_elem_t *alsaelement, const bool capture) +{ + QList result; + static const snd_mixer_selem_channel_id_t alsachanneltypes[] = { + SND_MIXER_SCHN_FRONT_LEFT, + SND_MIXER_SCHN_FRONT_RIGHT, + SND_MIXER_SCHN_REAR_LEFT, + SND_MIXER_SCHN_REAR_RIGHT, + SND_MIXER_SCHN_FRONT_CENTER, + SND_MIXER_SCHN_WOOFER, + SND_MIXER_SCHN_SIDE_LEFT, + SND_MIXER_SCHN_SIDE_RIGHT, + SND_MIXER_SCHN_REAR_CENTER, + SND_MIXER_SCHN_UNKNOWN + }; + + int counter = 0; + while (alsachanneltypes[counter] != SND_MIXER_SCHN_UNKNOWN) { + int alsaresult = 0; + if (!capture) { + alsaresult = snd_mixer_selem_has_playback_channel(alsaelement, alsachanneltypes[counter]); + } else { + alsaresult = snd_mixer_selem_has_capture_channel(alsaelement, alsachanneltypes[counter]); + } + if (alsaresult != 0) { + result.append(alsachanneltypes[counter]); + } + counter++; + } + return result; +} + +static bool kGetChannelVolumes(snd_mixer_elem_t *alsaelement, snd_mixer_selem_channel_id_t alsaelementchannel, const bool alsahascapture, + long *alsavolumemin, long *alsavolumemax, long *alsavolume) +{ + int alsaresult = 0; + if (alsahascapture) { + alsaresult = snd_mixer_selem_get_capture_volume_range(alsaelement, alsavolumemin, alsavolumemax); + if (alsaresult != 0) { + kWarning() << "Could not get capture channel volume range" << snd_strerror(alsaresult); + return false; + } + alsaresult = snd_mixer_selem_get_capture_volume(alsaelement, alsaelementchannel, alsavolume); + if (alsaresult != 0) { + kWarning() << "Could not get capture channel volume" << snd_strerror(alsaresult); + return false; + } + return true; + } + alsaresult = snd_mixer_selem_get_playback_volume_range(alsaelement, alsavolumemin, alsavolumemax); + if (alsaresult != 0) { + kWarning() << "Could not get playback channel volume range" << snd_strerror(alsaresult); + return false; + } + alsaresult = snd_mixer_selem_get_playback_volume(alsaelement, alsaelementchannel, alsavolume); + if (alsaresult != 0) { + kWarning() << "Could not get playback channel volume" << snd_strerror(alsaresult); + return false; + } + return true; +} + +static bool kIsMasterChannel(const QString &alsaelementname) +{ + return alsaelementname.contains(QLatin1String("master"), Qt::CaseInsensitive); +} + +static QString kIconForChannel(const QString &alsaelementname) +{ + // TODO: more icons + if (kIsMasterChannel(alsaelementname)) { + return QString::fromLatin1("mixer-master"); + } + if (alsaelementname.contains(QLatin1String("capture"), Qt::CaseInsensitive)) { + return QString::fromLatin1("mixer-capture"); + } + if (alsaelementname.contains(QLatin1String("pcm"), Qt::CaseInsensitive)) { + return QString::fromLatin1("mixer-pcm"); + } + if (alsaelementname.contains(QLatin1String("ac97"), Qt::CaseInsensitive)) { + return QString::fromLatin1("mixer-ac97"); + } + return QString::fromLatin1("mixer-line"); +} + +static int kFixedVolume(const int value, const int alsavolumemax) +{ + const qreal valuefactor = (qreal(alsavolumemax) / 100); + return qRound(qreal(value) / valuefactor); +} + +static int kVolumeStep() +{ + const int appscrolllines = QApplication::wheelScrollLines(); + return qMax(appscrolllines, 1); +} + +static int kStepVolume(const int value, const int maxvalue, const int step) +{ + const qreal valuefactor = (qreal(maxvalue) / 100); + return (value + (valuefactor * step)); +} + +static QIcon kMixerIcon(QObject *parent, const int value) +{ + QIcon result; + Plasma::Svg plasmasvg(parent); + plasmasvg.setImagePath("icons/audio"); + plasmasvg.setContainsMultipleImages(true); + if (plasmasvg.isValid()) { + QPixmap iconpixmap(s_svgiconsize, s_svgiconsize); + iconpixmap.fill(Qt::transparent); + QPainter iconpainter(&iconpixmap); + if (value >= 75) { + plasmasvg.paint(&iconpainter, iconpixmap.rect(), "audio-volume-high"); + } else if (value >= 50) { + plasmasvg.paint(&iconpainter, iconpixmap.rect(), "audio-volume-medium"); + } else if (value >= 25) { + plasmasvg.paint(&iconpainter, iconpixmap.rect(), "audio-volume-low"); + } else { + plasmasvg.paint(&iconpainter, iconpixmap.rect(), "audio-volume-muted"); + } + result = QIcon(iconpixmap); + } else { + result = KIcon(s_defaultpopupicon); + } + return result; +} + +class MixerTabWidget : public QGraphicsWidget +{ + Q_OBJECT +public: + MixerTabWidget(Plasma::TabBar *tabbar); + ~MixerTabWidget(); + + bool setup(const int cardnumber, const QByteArray &cardname); + QIcon mainVolumeIcon(); + void decreaseVolume(); + void increaseVolume(); + +Q_SIGNALS: + void mainVolumeChanged(); + +private Q_SLOTS: + void slotSliderMovedOrChanged(const int value); + +private: + QGraphicsLinearLayout* m_layout; + snd_mixer_t* m_alsamixer; + QString m_mainelement; + QList m_sliders; +}; + +MixerTabWidget::MixerTabWidget(Plasma::TabBar *tabbar) + : QGraphicsWidget(tabbar), + m_layout(nullptr), + m_alsamixer(nullptr) +{ + setMinimumSize(s_minimumsize); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + m_layout = new QGraphicsLinearLayout(Qt::Horizontal, this); + setLayout(m_layout); +} + +MixerTabWidget::~MixerTabWidget() +{ + if (m_alsamixer) { + snd_mixer_close(m_alsamixer); + } +} + +bool MixerTabWidget::setup(const int alsacard, const QByteArray &alsacardname) +{ + Q_ASSERT(m_alsamixer == nullptr); + int alsaresult = snd_mixer_open(&m_alsamixer, 0); + if (alsaresult != 0) { + kWarning() << "Could not open mixer" << snd_strerror(alsaresult); + return false; + } + alsaresult = snd_mixer_attach(m_alsamixer, alsacardname.constData()); + if (alsaresult != 0) { + kWarning() << "Could not attach mixer" << snd_strerror(alsaresult); + snd_mixer_close(m_alsamixer); + m_alsamixer = nullptr; + return false; + } + alsaresult = snd_mixer_selem_register(m_alsamixer, nullptr, nullptr); + if (alsaresult != 0) { + kWarning() << "Could not register mixer" << snd_strerror(alsaresult); + snd_mixer_close(m_alsamixer); + m_alsamixer = nullptr; + return false; + } + alsaresult = snd_mixer_load(m_alsamixer); + if (alsaresult != 0) { + kWarning() << "Could not load mixer" << snd_strerror(alsaresult); + snd_mixer_close(m_alsamixer); + m_alsamixer = nullptr; + return false; + } + + const int smalliconsize = KIconLoader::global()->currentSize(KIconLoader::Small); + const QSizeF smalliconsizef = QSizeF(smalliconsize, smalliconsize); + bool hasvalidelement = false; + QStringList alsaelementnames; + snd_mixer_elem_t *alsaelement = snd_mixer_first_elem(m_alsamixer); + for (; alsaelement; alsaelement = snd_mixer_elem_next(alsaelement)) { + if (snd_mixer_elem_empty(alsaelement)) { + continue; + } + const bool alsahasplayback = snd_mixer_selem_has_playback_volume(alsaelement); + const bool alsahascapture = snd_mixer_selem_has_capture_volume(alsaelement); + const uint alsaelementindex = snd_mixer_selem_get_index(alsaelement); + const QString alsaelementname = QString::fromLocal8Bit(snd_mixer_selem_get_name(alsaelement)); + if (!alsahasplayback && !alsahascapture) { + // no volume to mix + kDebug() << "Skipping" << alsaelementindex << alsaelementname << "due to lack of volume"; + continue; + } + // qDebug() << Q_FUNC_INFO << alsaelementindex << alsaelementname << alsahasplayback << alsahascapture; + + const QList alsaelementchannels = kALSAChannelTypes(alsaelement, alsahascapture); + if (alsaelementchannels.size() < 1) { + kWarning() << "Element has no channels" << alsaelementindex << alsaelementname; + continue; + } + + hasvalidelement = true; + Plasma::Frame* frame = new Plasma::Frame(this); + frame->setFrameShadow(Plasma::Frame::Sunken); + frame->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); + // TODO: maybe elide frame text + frame->setText(alsaelementname); + QGraphicsGridLayout* framelayout = new QGraphicsGridLayout(frame); + int columncounter = 0; + foreach (const snd_mixer_selem_channel_id_t alsaelementchannel, alsaelementchannels) { + long alsavolumemin = 0; + long alsavolumemax = 0; + long alsavolume = 0; + const bool gotvolumes = kGetChannelVolumes( + alsaelement, alsaelementchannel, alsahascapture, + &alsavolumemin, &alsavolumemax, &alsavolume + ); + if (!gotvolumes) { + continue; + } + const QString alsaelementchannelname = QString::fromLocal8Bit(snd_mixer_selem_channel_name(alsaelementchannel)); + Plasma::Slider* slider = new Plasma::Slider(frame); + slider->setProperty("_k_index", alsaelementindex); + slider->setProperty("_k_name", alsaelementname); + slider->setProperty("_k_channel", int(alsaelementchannel)); + slider->setProperty("_k_capture", alsahascapture); + slider->setOrientation(Qt::Vertical); + slider->setRange(int(alsavolumemin), int(alsavolumemax)); + slider->setValue(int(alsavolume)); + slider->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding); + slider->setMinimumSize(s_minimumslidersize); + slider->setToolTip(alsaelementchannelname); + connect( + slider, SIGNAL(sliderMoved(int)), + this, SLOT(slotSliderMovedOrChanged(int)) + ); + connect( + slider, SIGNAL(valueChanged(int)), + this, SLOT(slotSliderMovedOrChanged(int)) + ); + m_sliders.append(slider); + framelayout->addItem(slider, 0, columncounter, 1, 1); + columncounter++; + } + Plasma::IconWidget* iconwidget = new Plasma::IconWidget(frame); + iconwidget->setIcon(kIconForChannel(alsaelementname)); + iconwidget->setToolTip(alsaelementname); + iconwidget->setAcceptHoverEvents(false); + iconwidget->setMinimumIconSize(smalliconsizef); + iconwidget->setMaximumIconSize(smalliconsizef); + framelayout->addItem(iconwidget, 1, 0, 1, columncounter); + framelayout->setAlignment(iconwidget, Qt::AlignCenter); + frame->setLayout(framelayout); + + m_layout->addItem(frame); + columncounter++; + + if (m_mainelement.isEmpty() && kIsMasterChannel(alsaelementname)) { + m_mainelement = alsaelementname; + } else { + alsaelementnames.append(alsaelementname); + } + } + if (m_mainelement.isEmpty() && alsaelementnames.size() > 0) { + // pick the first as main if there is no master + m_mainelement = alsaelementnames.first(); + } + kDebug() << "Main element is" << m_mainelement; + m_layout->addStretch(); + + adjustSize(); + return hasvalidelement; +} + +QIcon MixerTabWidget::mainVolumeIcon() +{ + if (m_mainelement.isEmpty()) { + return KIcon(s_defaultpopupicon); + } + snd_mixer_elem_t *alsaelement = snd_mixer_first_elem(m_alsamixer); + for (; alsaelement; alsaelement = snd_mixer_elem_next(alsaelement)) { + if (snd_mixer_elem_empty(alsaelement)) { + continue; + } + const QString alsaelementname = QString::fromLocal8Bit(snd_mixer_selem_get_name(alsaelement)); + if (alsaelementname != m_mainelement) { + continue; + } + // the icon represents the highest volume from all element channels + int alsaelementvolume = 0; + const bool alsahascapture = snd_mixer_selem_has_capture_volume(alsaelement); + const QList alsaelementchannels = kALSAChannelTypes(alsaelement, alsahascapture); + foreach (const snd_mixer_selem_channel_id_t alsaelementchannel, alsaelementchannels) { + long alsavolumemin = 0; + long alsavolumemax = 0; + long alsavolume = 0; + const bool gotvolumes = kGetChannelVolumes( + alsaelement, alsaelementchannel, alsahascapture, + &alsavolumemin, &alsavolumemax, &alsavolume + ); + if (!gotvolumes) { + return KIcon(s_defaultpopupicon); + } + alsaelementvolume = qMax(alsaelementvolume, kFixedVolume(alsavolume, alsavolumemax)); + } + return kMixerIcon(this, alsaelementvolume); + } + return KIcon(s_defaultpopupicon); +} + +void MixerTabWidget::decreaseVolume() +{ + foreach (Plasma::Slider *slider, m_sliders) { + const QString alsaelementname = slider->property("_k_name").toString(); + if (alsaelementname == m_mainelement) { + slider->setValue(kStepVolume(slider->value(), slider->maximum(), -kVolumeStep())); + } + } +} +void MixerTabWidget::increaseVolume() +{ + foreach (Plasma::Slider *slider, m_sliders) { + const QString alsaelementname = slider->property("_k_name").toString(); + if (alsaelementname == m_mainelement) { + slider->setValue(kStepVolume(slider->value(), slider->maximum(), kVolumeStep())); + } + } +} + +void MixerTabWidget::slotSliderMovedOrChanged(const int value) +{ + Q_ASSERT(m_alsamixer != nullptr); + Plasma::Slider* slider = qobject_cast(sender()); + Q_ASSERT(slider != nullptr); + const uint alsaelementindex = slider->property("_k_index").toUInt(); + const int alsaelementchannel = slider->property("_k_channel").toInt(); + const bool alsahascapture = slider->property("_k_capture").toBool(); + snd_mixer_elem_t *alsaelement = snd_mixer_first_elem(m_alsamixer); + for (; alsaelement; alsaelement = snd_mixer_elem_next(alsaelement)) { + if (snd_mixer_elem_empty(alsaelement)) { + continue; + } + if (snd_mixer_selem_get_index(alsaelement) == alsaelementindex) { + kDebug() << "Changing" << alsaelementindex << "volume to" << value; + if (alsahascapture) { + const int alsaresult = snd_mixer_selem_set_capture_volume(alsaelement, snd_mixer_selem_channel_id_t(alsaelementchannel), long(value)); + if (alsaresult != 0) { + kWarning() << "Could not set capture volume" << snd_strerror(alsaresult); + return; + } + } else { + const int alsaresult = snd_mixer_selem_set_playback_volume(alsaelement, snd_mixer_selem_channel_id_t(alsaelementchannel), long(value)); + if (alsaresult != 0) { + kWarning() << "Could not set playback volume" << snd_strerror(alsaresult); + return; + } + } + const QString alsaelementname = QString::fromLocal8Bit(snd_mixer_selem_get_name(alsaelement)); + if (alsaelementname == m_mainelement) { + emit mainVolumeChanged(); + } + return; + } + } + kWarning() << "Could not find the element" << alsaelementindex; +} + + +class MixerWidget : public Plasma::TabBar +{ + Q_OBJECT +public: + MixerWidget(MixerApplet *mixer); + + void decreaseVolume(); + void increaseVolume(); + +public Q_SLOTS: + void slotUnhack(); + +protected: + // QGraphicsWidget reimplementation + QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint = QSizeF()) const final; + +private Q_SLOTS: + void slotMainVolumeChanged(); + void slotCurrentChanged(const int index); + +private: + MixerApplet* m_mixer; + bool m_hintshack; + QList m_tabwidgets; +}; + +MixerWidget::MixerWidget(MixerApplet* mixer) + : Plasma::TabBar(mixer), + m_mixer(mixer), + m_hintshack(true) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + setMinimumSize(s_minimumsize); + + int alsacard = -1; + while (true) { + int alsaresult = snd_card_next(&alsacard); + if (alsaresult != 0) { + kWarning() << "Could not get card" << snd_strerror(alsaresult); + break; + } + + const QByteArray alsacardname = (alsacard == -1 ? "default" : "hw:" + QByteArray::number(alsacard)); + snd_ctl_t *alsactl = nullptr; + alsaresult = snd_ctl_open(&alsactl, alsacardname.constData(), SND_CTL_NONBLOCK); + if (alsaresult != 0) { + kWarning() << "Could not open card" << snd_strerror(alsaresult); + break; + } + + snd_ctl_card_info_t *alsacardinfo = nullptr; + snd_ctl_card_info_alloca(&alsacardinfo); + alsaresult = snd_ctl_card_info(alsactl, alsacardinfo); + if (alsaresult != 0) { + kWarning() << "Could not open card" << snd_strerror(alsaresult); + snd_ctl_close(alsactl); + break; + } + + const QString alsamixername = QString::fromLocal8Bit(snd_ctl_card_info_get_mixername(alsacardinfo)); + snd_ctl_close(alsactl); + + MixerTabWidget* mixertabwidget = new MixerTabWidget(this); + if (mixertabwidget->setup(alsacard, alsacardname)) { + if (alsacard == -1) { + // default sound card goes to the front + insertTab(0, alsamixername, mixertabwidget); + m_tabwidgets.prepend(mixertabwidget); + } else { + addTab(alsamixername, mixertabwidget); + m_tabwidgets.append(mixertabwidget); + } + connect( + mixertabwidget, SIGNAL(mainVolumeChanged()), + this, SLOT(slotMainVolumeChanged()) + ); + } else { + delete mixertabwidget; + } + + if (alsacard == -1) { + break; + } + alsacard++; + } + + if (m_tabwidgets.size() > 0) { + m_mixer->setStatus(Plasma::ItemStatus::ActiveStatus); + } else { + m_mixer->setFailedToLaunch(true, i18n("No sound cards found")); + m_mixer->setStatus(Plasma::ItemStatus::PassiveStatus); + } + setTabBarShown(m_tabwidgets.size() > 1); + setCurrentIndex(0); + if (m_tabwidgets.size() > 0) { + m_mixer->setPopupIcon(m_tabwidgets.first()->mainVolumeIcon()); + } + connect( + this, SIGNAL(currentChanged(int)), + this, SLOT(slotCurrentChanged(int)) + ); +} + +void MixerWidget::decreaseVolume() +{ + if (m_tabwidgets.size() < 1) { + return; + } + MixerTabWidget* mixertabwidget = m_tabwidgets.at(currentIndex()); + mixertabwidget->decreaseVolume(); +} + +void MixerWidget::increaseVolume() +{ + if (m_tabwidgets.size() < 1) { + return; + } + MixerTabWidget* mixertabwidget = m_tabwidgets.at(currentIndex()); + mixertabwidget->increaseVolume(); +} + +void MixerWidget::slotUnhack() +{ + m_hintshack = false; +} + +QSizeF MixerWidget::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const +{ + // HACK: because Plasma::TabBar minimum size is bogus during initialization when there is nothing + // in it a hardcoded minimum size is returned, after that it is up to Plasma::TabBar and its + // child items to return something sensible (hint: that may not happen) + if (m_hintshack && which == Qt::MinimumSize) { + return s_minimumsize * 1.3; + } + return Plasma::TabBar::sizeHint(which, constraint); +} + +void MixerWidget::slotMainVolumeChanged() +{ + MixerTabWidget* mixertabwidget = qobject_cast(sender()); + if (m_tabwidgets.indexOf(mixertabwidget) == currentIndex()) { + m_mixer->setPopupIcon(mixertabwidget->mainVolumeIcon()); + } +} + +void MixerWidget::slotCurrentChanged(const int index) +{ + Q_ASSERT(index < m_tabwidgets.size()); + MixerTabWidget* mixertabwidget = m_tabwidgets.at(index); + Q_ASSERT(mixertabwidget != nullptr); + m_mixer->setPopupIcon(mixertabwidget->mainVolumeIcon()); +} + + +MixerApplet::MixerApplet(QObject *parent, const QVariantList &args) + : Plasma::PopupApplet(parent, args), + m_mixerwidget(nullptr) +{ + KGlobal::locale()->insertCatalog("plasma_applet_mixer"); + setAspectRatioMode(Plasma::AspectRatioMode::IgnoreAspectRatio); + m_mixerwidget = new MixerWidget(this); + setPopupIcon(s_defaultpopupicon); +} + +MixerApplet::~MixerApplet() +{ + delete m_mixerwidget; +} + +QGraphicsWidget* MixerApplet::graphicsWidget() +{ + return m_mixerwidget; +} + +void MixerApplet::wheelEvent(QGraphicsSceneWheelEvent *event) +{ + if (event->delta() < 0) { + m_mixerwidget->decreaseVolume(); + } else { + m_mixerwidget->increaseVolume(); + } +} + +void MixerApplet::constraintsEvent(Plasma::Constraints constraints) +{ + if (constraints & Plasma::FormFactorConstraint) { + switch (formFactor()) { + case Plasma::FormFactor::Horizontal: + case Plasma::FormFactor::Vertical: { + // HACK: limit the widget size to 2-times less than that of the desktop because + // Plasma::TabBar sets its maximum size to QWIDGETSIZE_MAX which is more than what + // can fit on panel and for some reason hints do not have effect on the widget size + // when it is in a panel, see: + // kdelibs/plasma/widgets/tabbar.cpp + const QSize desktopsize = qApp->desktop()->size(); + m_mixerwidget->setMaximumSize(desktopsize / 2); + break; + } + default: { + // back to the Plasma::TabBar maximum on form factor switch + m_mixerwidget->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); + break; + } + } + } + if (constraints & Plasma::StartupCompletedConstraint) { + QTimer::singleShot(500, m_mixerwidget, SLOT(slotUnhack())); + } +} + +K_EXPORT_PLASMA_APPLET(mixer, MixerApplet) + +#include "moc_mixer.cpp" +#include "mixer.moc" diff --git a/plasma/applets/mixer/mixer.h b/plasma/applets/mixer/mixer.h new file mode 100644 index 00000000..6e5feeff --- /dev/null +++ b/plasma/applets/mixer/mixer.h @@ -0,0 +1,48 @@ +/* This file is part of the KDE project + Copyright (C) 2023 Ivailo Monev + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2, as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef MIXER_H +#define MIXER_H + +#include +#include + +class MixerWidget; + +class MixerApplet : public Plasma::PopupApplet +{ + Q_OBJECT +public: + MixerApplet(QObject *parent, const QVariantList &args); + ~MixerApplet(); + + // Plasma::PopupApplet reimplementation + QGraphicsWidget* graphicsWidget() final; + // QGraphicsWidget reimplementation + void wheelEvent(QGraphicsSceneWheelEvent *event) final; + +protected: + // Plasma::Applet reimplementation + void constraintsEvent(Plasma::Constraints constraints) final; + +private: + friend MixerWidget; + MixerWidget *m_mixerwidget; +}; + +#endif // MIXER_H diff --git a/plasma/applets/mixer/plasma-applet-mixer.desktop b/plasma/applets/mixer/plasma-applet-mixer.desktop new file mode 100644 index 00000000..bd8a2c14 --- /dev/null +++ b/plasma/applets/mixer/plasma-applet-mixer.desktop @@ -0,0 +1,20 @@ +[Desktop Entry] +Encoding=UTF-8 +Name=Mixer +Comment=Sound Mixer + +Type=Service +Icon=audio-card +X-KDE-ParentApp= +X-KDE-PluginInfo-Author=Ivailo Monev +X-KDE-PluginInfo-Category=System Information +X-KDE-PluginInfo-Email=xakepa10@gmail.com +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=mixer +X-KDE-PluginInfo-Version=1.0 +X-KDE-PluginInfo-Website= +X-KDE-ServiceTypes=Plasma/PopupApplet,Plasma/Applet +X-KDE-Library=plasma_applet_mixer +X-Plasma-DefaultSize=290,140 +X-Plasma-RequiredExtensions=Unused +X-Plasma-NotificationArea=true diff --git a/plasma/applets/systemtray/ui/applet.cpp b/plasma/applets/systemtray/ui/applet.cpp index eddfc03b..b10ade1b 100644 --- a/plasma/applets/systemtray/ui/applet.cpp +++ b/plasma/applets/systemtray/ui/applet.cpp @@ -698,6 +698,10 @@ void Applet::checkDefaultApplets() } } + if (!applets.contains("mixer")) { + s_manager->addApplet("mixer", this); + } + config().writeEntry("DefaultAppletsAdded", true); }