diff --git a/CMakeLists.txt b/CMakeLists.txt
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -38,6 +38,7 @@
Plasma
)
find_package(PulseAudio 5.0.0 REQUIRED)
+find_package(Canberra REQUIRED)
find_package(GLIB2 REQUIRED)
add_subdirectory(applet)
diff --git a/applet/contents/config/main.xml b/applet/contents/config/main.xml
--- a/applet/contents/config/main.xml
+++ b/applet/contents/config/main.xml
@@ -12,6 +12,9 @@
5
+
+ true
+
diff --git a/applet/contents/ui/ConfigGeneral.qml b/applet/contents/ui/ConfigGeneral.qml
--- a/applet/contents/ui/ConfigGeneral.qml
+++ b/applet/contents/ui/ConfigGeneral.qml
@@ -22,38 +22,69 @@
import QtQuick.Layouts 1.0
import QtQuick.Controls 1.0
+import org.kde.plasma.private.volume 0.1
+
Item {
property alias cfg_maximumVolume: maximumVolume.value
property alias cfg_volumeStep: volumeStep.value
+ property alias cfg_volumeFeedback: volumeFeedback.checked
- GridLayout {
- columns: 2
+ ColumnLayout {
Layout.fillWidth: true
- Label {
- Layout.alignment: Qt.AlignRight
- text: i18n("Maximum volume:")
- }
+ GroupBox {
+ Layout.fillWidth: true
+ flat: true
+ title: i18n("Volume")
- SpinBox {
- id: maximumVolume
- minimumValue: 100
- maximumValue: 150
- stepSize: 1
- suffix: i18n("%")
- }
+ GridLayout {
+ columns: 2
+ Layout.fillWidth: true
+
+ Label {
+ Layout.alignment: Qt.AlignRight
+ text: i18n("Maximum volume:")
+ }
+
+ SpinBox {
+ id: maximumVolume
+ minimumValue: 100
+ maximumValue: 150
+ stepSize: 1
+ suffix: i18n("%")
+ }
- Label {
- Layout.alignment: Qt.AlignRight
- text: i18n("Volume step:")
+ Label {
+ Layout.alignment: Qt.AlignRight
+ text: i18n("Volume step:")
+ }
+
+ SpinBox {
+ id: volumeStep
+ minimumValue: 1
+ maximumValue: 100
+ stepSize: 1
+ suffix: i18n("%")
+ }
+ }
}
- SpinBox {
- id: volumeStep
- minimumValue: 1
- maximumValue: 100
- stepSize: 1
- suffix: i18n("%")
+ GroupBox {
+ Layout.fillWidth: true
+ flat: true
+ title: i18n("Behavior")
+
+ ColumnLayout {
+ CheckBox {
+ id: volumeFeedback
+ text: i18n("Volume feedback")
+ enabled: feedback.valid
+ }
+ }
}
}
+
+ VolumeFeedback {
+ id: feedback
+ }
}
diff --git a/applet/contents/ui/ListItemBase.qml b/applet/contents/ui/ListItemBase.qml
--- a/applet/contents/ui/ListItemBase.qml
+++ b/applet/contents/ui/ListItemBase.qml
@@ -158,6 +158,10 @@
Volume = value;
Muted = false;
+ if (type == "sink") {
+ playFeedback(CardIndex);
+ }
+
if (!pressed) {
updateTimer.restart();
}
diff --git a/applet/contents/ui/main.qml b/applet/contents/ui/main.qml
--- a/applet/contents/ui/main.qml
+++ b/applet/contents/ui/main.qml
@@ -33,6 +33,7 @@
Item {
id: main
+ property bool volumeFeedback: Plasmoid.configuration.volumeFeedback
property int maxVolumePercent: Plasmoid.configuration.maximumVolume
property int maxVolumeValue: Math.round(maxVolumePercent * PulseAudio.NormalVolume / 100.0)
property int volumeStep: Math.round(Plasmoid.configuration.volumeStep * PulseAudio.NormalVolume / 100.0)
@@ -69,6 +70,7 @@
sinkModel.preferredSink.muted = false;
sinkModel.preferredSink.volume = volume;
osd.show(volumePercent(volume, maxVolumeValue));
+ playFeedback();
}
function decreaseVolume() {
@@ -79,6 +81,7 @@
sinkModel.preferredSink.muted = false;
sinkModel.preferredSink.volume = volume;
osd.show(volumePercent(volume, maxVolumeValue));
+ playFeedback();
}
function muteVolume() {
@@ -88,6 +91,7 @@
var toMute = !sinkModel.preferredSink.muted;
sinkModel.preferredSink.muted = toMute;
osd.show(toMute ? 0 : volumePercent(sinkModel.preferredSink.volume, maxVolumeValue));
+ playFeedback();
}
function increaseMicrophoneVolume() {
@@ -140,6 +144,16 @@
sinkViewHeader.visible = true;
}
+ function playFeedback(sinkIndex) {
+ if (!volumeFeedback) {
+ return;
+ }
+ if (!sinkIndex) {
+ sinkIndex = sinkModel.preferredSink.cardIndex;
+ }
+ feedback.play(sinkIndex);
+ }
+
Plasmoid.compactRepresentation: PlasmaCore.IconItem {
source: plasmoid.icon
active: mouseArea.containsMouse
@@ -237,6 +251,10 @@
id: osd
}
+ VolumeFeedback {
+ id: feedback
+ }
+
PlasmaComponents.TabBar {
id: tabBar
diff --git a/cmake/FindCanberra.cmake b/cmake/FindCanberra.cmake
new file mode 100644
--- /dev/null
+++ b/cmake/FindCanberra.cmake
@@ -0,0 +1,50 @@
+# - Find libcanberra's libraries and headers.
+# This module defines the following variables:
+#
+# CANBERRA_FOUND - true if libcanberra was found
+# CANBERRA_LIBRARIES - libcanberra libraries to link against
+# CANBERRA_INCLUDE_DIRS - include path for libcanberra
+#
+# Copyright (c) 2012 Raphael Kubo da Costa
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. Neither the name of the University nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+find_package(PkgConfig)
+pkg_check_modules(PC_CANBERRA libcanberra)
+
+find_library(CANBERRA_LIBRARIES
+ NAMES canberra
+ HINTS ${PC_CANBERRA_LIBRARY_DIRS} ${PC_CANBERRA_LIBDIR}
+)
+
+find_path(CANBERRA_INCLUDE_DIRS
+ NAMES canberra.h
+ HINTS ${PC_CANBERRA_INCLUDE_DIRS} ${PC_CANBERRA_INCLUDEDIR}
+)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(Canberra REQUIRED_VARS CANBERRA_LIBRARIES CANBERRA_INCLUDE_DIRS)
+
+mark_as_advanced(CANBERRA_LIBRARIES CANBERRA_INCLUDE_DIRS)
diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt
--- a/src/qml/CMakeLists.txt
+++ b/src/qml/CMakeLists.txt
@@ -7,6 +7,7 @@
globalactioncollection.cpp
plugin.cpp
volumeosd.cpp
+ volumefeedback.cpp
)
set_property(SOURCE dbus/osdService.xml APPEND PROPERTY CLASSNAME OsdServiceInterface)
@@ -18,6 +19,7 @@
Qt5::Quick
KF5::GlobalAccel
QPulseAudioPrivate
+ ${CANBERRA_LIBRARIES}
)
set(PRIVATE_QML_INSTALL_DIR ${QML_INSTALL_DIR}/org/kde/plasma/private/volume)
diff --git a/src/qml/plugin.cpp b/src/qml/plugin.cpp
--- a/src/qml/plugin.cpp
+++ b/src/qml/plugin.cpp
@@ -31,6 +31,7 @@
#include "globalactioncollection.h"
#include "volumeosd.h"
+#include "volumefeedback.h"
static QJSValue pulseaudio_singleton(QQmlEngine *engine, QJSEngine *scriptEngine)
{
@@ -55,6 +56,7 @@
qmlRegisterType(uri, 0, 1, "GlobalAction");
qmlRegisterType(uri, 0, 1, "GlobalActionCollection");
qmlRegisterType(uri, 0, 1, "VolumeOSD");
+ qmlRegisterType(uri, 0, 1, "VolumeFeedback");
qmlRegisterSingletonType(uri, 0, 1, "PulseAudio", pulseaudio_singleton);
qmlRegisterType();
diff --git a/src/qml/volumefeedback.h b/src/qml/volumefeedback.h
new file mode 100644
--- /dev/null
+++ b/src/qml/volumefeedback.h
@@ -0,0 +1,46 @@
+/*
+ Copyright 2016 David Rosca
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) version 3, or any
+ later version accepted by the membership of KDE e.V. (or its
+ successor approved by the membership of KDE e.V.), which shall
+ act as a proxy defined in Section 6 of version 3 of the license.
+
+ 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library. If not, see .
+*/
+
+#ifndef VOLUMEFEEDBACK_H
+#define VOLUMEFEEDBACK_H
+
+#include
+
+#include
+
+class VolumeFeedback : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(bool valid READ isValid CONSTANT)
+
+public:
+ explicit VolumeFeedback(QObject *parent = nullptr);
+ ~VolumeFeedback();
+
+ bool isValid() const;
+
+public slots:
+ void play(quint32 sinkIndex);
+
+private:
+ ca_context *m_context = nullptr;
+};
+
+#endif // VOLUMEFEEDBACK_H
diff --git a/src/qml/volumefeedback.cpp b/src/qml/volumefeedback.cpp
new file mode 100644
--- /dev/null
+++ b/src/qml/volumefeedback.cpp
@@ -0,0 +1,83 @@
+/*
+ Copyright 2008 Helio Chissini de Castro
+ Copyright 2016 David Rosca
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) version 3, or any
+ later version accepted by the membership of KDE e.V. (or its
+ successor approved by the membership of KDE e.V.), which shall
+ act as a proxy defined in Section 6 of version 3 of the license.
+
+ 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library. If not, see .
+*/
+
+#include "volumefeedback.h"
+
+VolumeFeedback::VolumeFeedback(QObject *parent)
+ : QObject(parent)
+{
+ if (ca_context_create(&m_context) < 0) {
+ m_context = nullptr;
+ return;
+ }
+ if (ca_context_set_driver(m_context, "pulse") < 0) {
+ ca_context_destroy(m_context);
+ m_context = nullptr;
+ return;
+ }
+}
+
+VolumeFeedback::~VolumeFeedback()
+{
+ if (m_context) {
+ ca_context_destroy(m_context);
+ }
+}
+
+bool VolumeFeedback::isValid() const
+{
+ return m_context;
+}
+
+void VolumeFeedback::play(quint32 sinkIndex)
+{
+ if (!m_context) {
+ return;
+ }
+
+ int playing = 0;
+ const int cindex = 2; // Note "2" is simply the index we've picked. It's somewhat irrelevant.
+ ca_context_playing(m_context, cindex, &playing);
+
+ // NB Depending on how this is desired to work, we may want to simply
+ // skip playing, or cancel the currently playing sound and play our
+ // new one... for now, let's do the latter.
+ if (playing) {
+ ca_context_cancel(m_context, cindex);
+ }
+
+ char dev[64];
+ snprintf(dev, sizeof(dev), "%lu", (unsigned long) sinkIndex);
+ ca_context_change_device(m_context, dev);
+
+ // Ideally we'd use something like ca_gtk_play_for_widget()...
+ ca_context_play(
+ m_context,
+ cindex,
+ CA_PROP_EVENT_DESCRIPTION, "Volume Control Feedback Sound",
+ CA_PROP_EVENT_ID, "audio-volume-change",
+ CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
+ CA_PROP_CANBERRA_ENABLE, "1",
+ nullptr
+ );
+
+ ca_context_change_device(m_context, nullptr);
+}