diff --git a/CMakeLists.txt b/CMakeLists.txt
index da99f74..8936fca 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,48 +1,49 @@
project(plasma-volume-control)
cmake_minimum_required(VERSION 2.8.12)
set(PROJECT_VERSION "5.7.90")
set(PROJECT_VERSION_MAJOR 5)
set (QT_MIN_VERSION "5.4.0")
set (ECM_MIN_VERSION "0.0.14")
find_package(ECM ${ECM_MIN_VERSION} REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/)
add_definitions(-DTRANSLATION_DOMAIN=\"kcm_pulseaudio\")
include(FeatureSummary)
include(KDEInstallDirs)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(ECMOptionalAddSubdirectory)
include(FindPkgConfig)
pkg_check_modules(GCONF REQUIRED gconf-2.0)
pkg_check_modules(GOBJECT REQUIRED gobject-2.0)
find_package(Qt5 ${QT_MIN_VERSION} REQUIRED COMPONENTS
Core
Gui
DBus
Widgets
Quick
)
find_package(KF5 REQUIRED COMPONENTS
CoreAddons
Declarative
DocTools
GlobalAccel
I18n
Plasma
)
find_package(PulseAudio 5.0.0 REQUIRED)
+find_package(Canberra REQUIRED)
find_package(GLIB2 REQUIRED)
add_subdirectory(applet)
add_subdirectory(src)
add_subdirectory(data)
add_subdirectory(doc)
feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
diff --git a/applet/contents/config/main.xml b/applet/contents/config/main.xml
index dd6f6ce..861dab1 100644
--- a/applet/contents/config/main.xml
+++ b/applet/contents/config/main.xml
@@ -1,17 +1,20 @@
100
5
+
+ true
+
diff --git a/applet/contents/ui/ConfigGeneral.qml b/applet/contents/ui/ConfigGeneral.qml
index 8677a84..9d85bd2 100644
--- a/applet/contents/ui/ConfigGeneral.qml
+++ b/applet/contents/ui/ConfigGeneral.qml
@@ -1,59 +1,90 @@
/*
Copyright 2016 David Rosca
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 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 14 of version 3 of the license.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
import QtQuick 2.0
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
index a0d8d2d..a16079d 100644
--- a/applet/contents/ui/ListItemBase.qml
+++ b/applet/contents/ui/ListItemBase.qml
@@ -1,202 +1,206 @@
/*
Copyright 2014-2015 Harald Sitter
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 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 14 of version 3 of the license.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
import QtQuick 2.4
import QtQuick.Controls 1.0
import QtQuick.Layouts 1.0
import org.kde.kquickcontrolsaddons 2.0
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.draganddrop 2.0 as DragAndDrop
import org.kde.plasma.private.volume 0.1
PlasmaComponents.ListItem {
id: item
property alias label: textLabel.text
property alias draggable: dragArea.enabled
property alias icon: clientIcon.source
property string type
anchors {
left: parent.left;
right: parent.right;
}
checked: dropArea.containsDrag
opacity: (draggedStream && draggedStream.deviceIndex == Index) ? 0.3 : 1.0
DragAndDrop.DropArea {
id: dropArea
anchors.fill: parent
enabled: draggedStream
onDragEnter: {
if (draggedStream.deviceIndex == Index) {
event.ignore();
}
}
onDrop: {
draggedStream.deviceIndex = Index;
}
}
ColumnLayout {
property int maximumWidth: parent.width
width: maximumWidth
Layout.maximumWidth: maximumWidth
RowLayout {
Layout.fillWidth: true
spacing: units.smallSpacing
PlasmaCore.IconItem {
id: clientIcon
visible: valid
Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: column.height * 0.75
Layout.preferredWidth: Layout.preferredHeight
DragAndDrop.DragArea {
id: dragArea
anchors.fill: parent
delegate: parent
mimeData {
source: item
}
onDragStarted: {
draggedStream = PulseObject;
main.beginMoveStream(type == "sink-input" ? "sink" : "source");
}
onDrop: {
draggedStream = null;
main.endMoveStream();
}
MouseArea {
anchors.fill: parent
cursorShape: dragArea.enabled ? (pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor) : undefined
}
}
}
ColumnLayout {
id: column
PlasmaExtras.Heading {
id :textLabel
Layout.fillWidth: true
level: 5
opacity: 0.6
wrapMode: Text.NoWrap
elide: Text.ElideRight
}
RowLayout {
VolumeIcon {
Layout.maximumHeight: slider.height * 0.75
Layout.maximumWidth: slider.height* 0.75
volume: Volume
muted: Muted
MouseArea {
anchors.fill: parent
onPressed: Muted = !Muted
}
}
PlasmaComponents.Slider {
id: slider
// Helper properties to allow async slider updates.
// While we are sliding we must not react to value updates
// as otherwise we can easily end up in a loop where value
// changes trigger volume changes trigger value changes.
property int volume: Volume
property bool ignoreValueChange: false
Layout.fillWidth: true
minimumValue: PulseAudio.MinimalVolume
maximumValue: maxVolumeValue
stepSize: maximumValue / maxVolumePercent
visible: HasVolume
enabled: VolumeWritable
opacity: Muted ? 0.5 : 1
onVolumeChanged: {
ignoreValueChange = true;
value = Volume;
ignoreValueChange = false;
}
onValueChanged: {
if (!ignoreValueChange) {
Volume = value;
Muted = false;
+ if (type == "sink") {
+ playFeedback(CardIndex);
+ }
+
if (!pressed) {
updateTimer.restart();
}
}
}
onPressedChanged: {
if (!pressed) {
// Make sure to sync the volume once the button was
// released.
// Otherwise it might be that the slider is at v10
// whereas PA rejected the volume change and is
// still at v15 (e.g.).
updateTimer.restart();
}
}
Timer {
id: updateTimer
interval: 200
onTriggered: slider.value = Volume
}
}
PlasmaComponents.Label {
id: percentText
readonly property real value: PulseObject.volume > slider.maximumValue ? PulseObject.volume : slider.value
Layout.alignment: Qt.AlignHCenter
Layout.minimumWidth: percentMetrics.advanceWidth
horizontalAlignment: Qt.AlignRight
text: i18nc("volume percentage", "%1%", Math.round(value / PulseAudio.NormalVolume * 100.0))
}
TextMetrics {
id: percentMetrics
font: percentText.font
text: i18nc("only used for sizing, should be widest possible string", "100%")
}
}
}
}
}
}
diff --git a/applet/contents/ui/main.qml b/applet/contents/ui/main.qml
index 69911ff..0acafc8 100644
--- a/applet/contents/ui/main.qml
+++ b/applet/contents/ui/main.qml
@@ -1,393 +1,411 @@
/*
Copyright 2014-2015 Harald Sitter
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 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 14 of version 3 of the license.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
import QtQuick 2.0
import QtQuick.Layouts 1.0
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.private.volume 0.1
import "../code/icon.js" as Icon
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)
property string displayName: i18n("Audio Volume")
property QtObject draggedStream: null
Layout.minimumHeight: units.gridUnit * 12
Layout.minimumWidth: units.gridUnit * 12
Layout.preferredHeight: units.gridUnit * 20
Layout.preferredWidth: units.gridUnit * 20
Plasmoid.icon: sinkModel.preferredSink ? Icon.name(sinkModel.preferredSink.volume, sinkModel.preferredSink.muted) : Icon.name(0, true)
Plasmoid.switchWidth: units.gridUnit * 12
Plasmoid.switchHeight: units.gridUnit * 12
Plasmoid.toolTipMainText: displayName
Plasmoid.toolTipSubText: sinkModel.preferredSink ? i18n("Volume at %1%\n%2", volumePercent(sinkModel.preferredSink.volume), sinkModel.preferredSink.description) : ""
function boundVolume(volume) {
return Math.max(PulseAudio.MinimalVolume, Math.min(volume, maxVolumeValue));
}
function volumePercent(volume, max) {
if (!max) {
max = PulseAudio.NormalVolume;
}
return Math.round(volume / max * 100.0);
}
function increaseVolume() {
if (!sinkModel.preferredSink) {
return;
}
var volume = boundVolume(sinkModel.preferredSink.volume + volumeStep);
sinkModel.preferredSink.muted = false;
sinkModel.preferredSink.volume = volume;
osd.show(volumePercent(volume, maxVolumeValue));
+ playFeedback();
}
function decreaseVolume() {
if (!sinkModel.preferredSink) {
return;
}
var volume = boundVolume(sinkModel.preferredSink.volume - volumeStep);
sinkModel.preferredSink.muted = false;
sinkModel.preferredSink.volume = volume;
osd.show(volumePercent(volume, maxVolumeValue));
+ playFeedback();
}
function muteVolume() {
if (!sinkModel.preferredSink) {
return;
}
var toMute = !sinkModel.preferredSink.muted;
sinkModel.preferredSink.muted = toMute;
osd.show(toMute ? 0 : volumePercent(sinkModel.preferredSink.volume, maxVolumeValue));
+ playFeedback();
}
function increaseMicrophoneVolume() {
if (!sourceModel.defaultSource) {
return;
}
var volume = boundVolume(sourceModel.defaultSource.volume + volumeStep);
sourceModel.defaultSource.muted = false;
sourceModel.defaultSource.volume = volume;
osd.showMicrophone(volumePercent(volume));
}
function decreaseMicrophoneVolume() {
if (!sourceModel.defaultSource) {
return;
}
var volume = boundVolume(sourceModel.defaultSource.volume - volumeStep);
sourceModel.defaultSource.muted = false;
sourceModel.defaultSource.volume = volume;
osd.showMicrophone(volumePercent(volume));
}
function muteMicrophone() {
if (!sourceModel.defaultSource) {
return;
}
var toMute = !sourceModel.defaultSource.muted;
sourceModel.defaultSource.muted = toMute;
osd.showMicrophone(toMute? 0 : volumePercent(sourceModel.defaultSource.volume));
}
function beginMoveStream(type, stream) {
if (type == "sink") {
sourceView.visible = false;
sourceViewHeader.visible = false;
} else if (type == "source") {
sinkView.visible = false;
sinkViewHeader.visible = false;
}
tabBar.currentTab = devicesTab;
}
function endMoveStream() {
tabBar.currentTab = streamsTab;
sourceView.visible = true;
sourceViewHeader.visible = true;
sinkView.visible = true;
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
colorGroup: PlasmaCore.ColorScope.colorGroup
MouseArea {
id: mouseArea
property int wheelDelta: 0
property bool wasExpanded: false
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
onPressed: {
if (mouse.button == Qt.LeftButton) {
wasExpanded = plasmoid.expanded;
} else if (mouse.button == Qt.MiddleButton) {
muteVolume();
}
}
onClicked: {
if (mouse.button == Qt.LeftButton) {
plasmoid.expanded = !wasExpanded;
}
}
onWheel: {
var delta = wheel.angleDelta.y || wheel.angleDelta.x;
wheelDelta += delta;
// Magic number 120 for common "one click"
// See: http://qt-project.org/doc/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop
while (wheelDelta >= 120) {
wheelDelta -= 120;
increaseVolume();
}
while (wheelDelta <= -120) {
wheelDelta += 120;
decreaseVolume();
}
}
}
}
GlobalActionCollection {
// KGlobalAccel cannot transition from kmix to something else, so if
// the user had a custom shortcut set for kmix those would get lost.
// To avoid this we hijack kmix name and actions. Entirely mental but
// best we can do to not cause annoyance for the user.
// The display name actually is updated to whatever registered last
// though, so as far as user visible strings go we should be fine.
// As of 2015-07-21:
// componentName: kmix
// actions: increase_volume, decrease_volume, mute
name: "kmix"
displayName: main.displayName
GlobalAction {
objectName: "increase_volume"
text: i18n("Increase Volume")
shortcut: Qt.Key_VolumeUp
onTriggered: increaseVolume()
}
GlobalAction {
objectName: "decrease_volume"
text: i18n("Decrease Volume")
shortcut: Qt.Key_VolumeDown
onTriggered: decreaseVolume()
}
GlobalAction {
objectName: "mute"
text: i18n("Mute")
shortcut: Qt.Key_VolumeMute
onTriggered: muteVolume()
}
GlobalAction {
objectName: "increase_microphone_volume"
text: i18n("Increase Microphone Volume")
shortcut: Qt.Key_MicVolumeUp
onTriggered: increaseMicrophoneVolume()
}
GlobalAction {
objectName: "decrease_microphone_volume"
text: i18n("Decrease Microphone Volume")
shortcut: Qt.Key_MicVolumeDown
onTriggered: decreaseMicrophoneVolume()
}
GlobalAction {
objectName: "mic_mute"
text: i18n("Mute Microphone")
shortcut: Qt.Key_MicMute
onTriggered: muteMicrophone()
}
}
VolumeOSD {
id: osd
}
+ VolumeFeedback {
+ id: feedback
+ }
+
PlasmaComponents.TabBar {
id: tabBar
anchors {
top: parent.top
left: parent.left
right: parent.right
}
PlasmaComponents.TabButton {
id: devicesTab
text: i18n("Devices")
}
PlasmaComponents.TabButton {
id: streamsTab
text: i18n("Applications")
}
}
PlasmaExtras.ScrollArea {
id: scrollView;
anchors {
top: tabBar.bottom
topMargin: units.smallSpacing
left: parent.left
right: parent.right
bottom: parent.bottom
}
horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff
flickableItem.boundsBehavior: Flickable.StopAtBounds;
Item {
width: streamsView.visible ? streamsView.width : devicesView.width
height: streamsView.visible ? streamsView.height : devicesView.height
ColumnLayout {
id: streamsView
visible: tabBar.currentTab == streamsTab
property int maximumWidth: scrollView.viewport.width
width: maximumWidth
Layout.maximumWidth: maximumWidth
Header {
Layout.fillWidth: true
visible: sinkInputView.count > 0
text: i18n("Playback Streams")
}
ListView {
id: sinkInputView
Layout.fillWidth: true
Layout.minimumHeight: contentHeight
Layout.maximumHeight: contentHeight
model: PulseObjectFilterModel {
filters: [ { role: "VirtualStream", value: false } ]
sourceModel: SinkInputModel {}
}
boundsBehavior: Flickable.StopAtBounds;
delegate: StreamListItem {
type: "sink-input"
draggable: sinkView.count > 1
}
}
Header {
Layout.fillWidth: true
visible: sourceOutputView.count > 0
text: i18n("Capture Streams")
}
ListView {
id: sourceOutputView
Layout.fillWidth: true
Layout.minimumHeight: contentHeight
Layout.maximumHeight: contentHeight
model: PulseObjectFilterModel {
filters: [ { role: "VirtualStream", value: false } ]
sourceModel: SourceOutputModel {}
}
boundsBehavior: Flickable.StopAtBounds;
delegate: StreamListItem {
type: "source-input"
draggable: sourceView.count > 1
}
}
}
ColumnLayout {
id: devicesView
visible: tabBar.currentTab == devicesTab
property int maximumWidth: scrollView.viewport.width
width: maximumWidth
Layout.maximumWidth: maximumWidth
Header {
id: sinkViewHeader
Layout.fillWidth: true
visible: sinkView.count > 0
text: i18n("Playback Devices")
}
ListView {
id: sinkView
Layout.fillWidth: true
Layout.minimumHeight: contentHeight
Layout.maximumHeight: contentHeight
model: PulseObjectFilterModel {
sortRole: "SortByDefault"
sortOrder: Qt.DescendingOrder
sourceModel: SinkModel {
id: sinkModel
}
}
boundsBehavior: Flickable.StopAtBounds;
delegate: DeviceListItem {
type: "sink"
}
}
Header {
id: sourceViewHeader
Layout.fillWidth: true
visible: sourceView.count > 0
text: i18n("Capture Devices")
}
ListView {
id: sourceView
Layout.fillWidth: true
Layout.minimumHeight: contentHeight
Layout.maximumHeight: contentHeight
model: PulseObjectFilterModel {
sortRole: "SortByDefault"
sortOrder: Qt.DescendingOrder
sourceModel: SourceModel {
id: sourceModel
}
}
boundsBehavior: Flickable.StopAtBounds;
delegate: DeviceListItem {
type: "source"
}
}
}
}
}
}
diff --git a/cmake/FindCanberra.cmake b/cmake/FindCanberra.cmake
new file mode 100644
index 0000000..48e2d54
--- /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
index 30202d5..3265bca 100644
--- a/src/qml/CMakeLists.txt
+++ b/src/qml/CMakeLists.txt
@@ -1,25 +1,27 @@
set(qml_SRCS
qmldir
PulseObjectFilterModel.qml
)
set(cpp_SRCS
globalactioncollection.cpp
plugin.cpp
volumeosd.cpp
+ volumefeedback.cpp
)
set_property(SOURCE dbus/osdService.xml APPEND PROPERTY CLASSNAME OsdServiceInterface)
qt5_add_dbus_interface(dbus_SRCS dbus/osdService.xml osdservice)
add_library(plasma-volume-declarative SHARED ${dbus_SRCS} ${cpp_SRCS} ${qml_SRCS})
target_link_libraries(plasma-volume-declarative
Qt5::DBus
Qt5::Quick
KF5::GlobalAccel
QPulseAudioPrivate
+ ${CANBERRA_LIBRARIES}
)
set(PRIVATE_QML_INSTALL_DIR ${QML_INSTALL_DIR}/org/kde/plasma/private/volume)
install(TARGETS plasma-volume-declarative DESTINATION ${PRIVATE_QML_INSTALL_DIR})
install(FILES ${qml_SRCS} DESTINATION ${PRIVATE_QML_INSTALL_DIR})
diff --git a/src/qml/plugin.cpp b/src/qml/plugin.cpp
index be54db8..e4604df 100644
--- a/src/qml/plugin.cpp
+++ b/src/qml/plugin.cpp
@@ -1,63 +1,65 @@
/*
Copyright 2014-2015 Harald Sitter
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 "plugin.h"
#include
#include "pulseaudio.h"
#include "client.h"
#include "sink.h"
#include "source.h"
#include "context.h"
#include "modulemanager.h"
#include "globalactioncollection.h"
#include "volumeosd.h"
+#include "volumefeedback.h"
static QJSValue pulseaudio_singleton(QQmlEngine *engine, QJSEngine *scriptEngine)
{
Q_UNUSED(engine)
QJSValue object = scriptEngine->newObject();
object.setProperty(QStringLiteral("NormalVolume"), (double) QPulseAudio::Context::NormalVolume);
object.setProperty(QStringLiteral("MinimalVolume"), (double) QPulseAudio::Context::MinimalVolume);
object.setProperty(QStringLiteral("MaximalVolume"), (double) QPulseAudio::Context::MaximalVolume);
return object;
}
void Plugin::registerTypes(const char* uri)
{
qmlRegisterType(uri, 0, 1, "CardModel");
qmlRegisterType(uri, 0, 1, "SinkModel");
qmlRegisterType(uri, 0, 1, "SinkInputModel");
qmlRegisterType(uri, 0, 1, "SourceModel");
qmlRegisterType(uri, 0, 1, "ModuleManager");
qmlRegisterType(uri, 0, 1, "SourceOutputModel");
qmlRegisterType(uri, 0, 1, "StreamRestoreModel");
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();
qmlRegisterType();
qmlRegisterType();
}
diff --git a/src/qml/volumefeedback.cpp b/src/qml/volumefeedback.cpp
new file mode 100644
index 0000000..2c9f6c7
--- /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);
+}
diff --git a/src/qml/volumefeedback.h b/src/qml/volumefeedback.h
new file mode 100644
index 0000000..ed1bfe0
--- /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