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