diff --git a/applet/contents/code/icon.js b/applet/contents/code/icon.js index 6ed8a70..7f592b9 100644 --- a/applet/contents/code/icon.js +++ b/applet/contents/code/icon.js @@ -1,35 +1,34 @@ /* 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 . */ function name(volume, muted) { - // FIXME: hardcoded max value var icon = null; - var percent = volume / 65536; + var percent = volume / maxVolumeValue; if (percent <= 0.0 || muted) { icon = "audio-volume-muted"; } else if (percent <= 0.25) { icon = "audio-volume-low"; } else if (percent <= 0.75) { icon = "audio-volume-medium"; } else { icon = "audio-volume-high"; } return icon; } diff --git a/applet/contents/ui/ListItemBase.qml b/applet/contents/ui/ListItemBase.qml index ec8570b..49693bf 100644 --- a/applet/contents/ui/ListItemBase.qml +++ b/applet/contents/ui/ListItemBase.qml @@ -1,199 +1,200 @@ /* 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: 0 - // FIXME: I do wonder if exposing max through the model would be useful at all - maximumValue: 65536 - stepSize: maximumValue / 100 + minimumValue: PulseAudio.MinimalVolume + maximumValue: maxVolumeValue + stepSize: maximumValue / maxVolumePercent visible: HasVolume enabled: VolumeWritable && !Muted onVolumeChanged: { ignoreValueChange = true; value = Volume; ignoreValueChange = false; } onValueChanged: { if (!ignoreValueChange) { Volume = value; 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.floor(slider.value / slider.maximumValue * 100.0)) + 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/VolumeIcon.qml b/applet/contents/ui/VolumeIcon.qml index cb43dd9..01f67eb 100644 --- a/applet/contents/ui/VolumeIcon.qml +++ b/applet/contents/ui/VolumeIcon.qml @@ -1,34 +1,36 @@ /* 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.private.volume 0.1 + import "../code/icon.js" as Icon PlasmaCore.SvgItem { property int volume property bool muted svg: PlasmaCore.Svg { imagePath: "icons/audio" } elementId: Icon.name(volume, muted) } diff --git a/applet/contents/ui/main.qml b/applet/contents/ui/main.qml index 75f84d2..b1f91e0 100644 --- a/applet/contents/ui/main.qml +++ b/applet/contents/ui/main.qml @@ -1,384 +1,389 @@ /* 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 int volumeStep: 65536 / 15 + property int maxVolumePercent: 100 + property int maxVolumeValue: Math.round(maxVolumePercent * PulseAudio.NormalVolume / 100.0) + property int volumeStep: PulseAudio.NormalVolume / 15 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.defaultSink ? Icon.name(sinkModel.defaultSink.volume, sinkModel.defaultSink.muted) : Icon.name(0, true) Plasmoid.switchWidth: units.gridUnit * 12 Plasmoid.switchHeight: units.gridUnit * 12 Plasmoid.toolTipMainText: displayName Plasmoid.toolTipSubText: sinkModel.defaultSink ? i18n("Volume at %1%\n%2", volumePercent(sinkModel.defaultSink.volume), sinkModel.defaultSink.description) : "" - function bound(value, min, max) { - return Math.max(min, Math.min(value, max)); + function boundVolume(volume) { + return Math.max(PulseAudio.MinimalVolume, Math.min(volume, maxVolumeValue)); } - function volumePercent(volume) { - return Math.round(100 * volume / 65536); + function volumePercent(volume, max) { + if (!max) { + max = PulseAudio.NormalVolume; + } + return Math.round(volume / max * 100.0); } function increaseVolume() { if (!sinkModel.defaultSink) { return; } - var volume = bound(sinkModel.defaultSink.volume + volumeStep, 0, 65536); + var volume = boundVolume(sinkModel.defaultSink.volume + volumeStep); sinkModel.defaultSink.volume = volume; - osd.show(volumePercent(volume)); + osd.show(volumePercent(volume, maxVolumeValue)); } function decreaseVolume() { if (!sinkModel.defaultSink) { return; } - var volume = bound(sinkModel.defaultSink.volume - volumeStep, 0, 65536); + var volume = boundVolume(sinkModel.defaultSink.volume - volumeStep); sinkModel.defaultSink.volume = volume; - osd.show(volumePercent(volume)); + osd.show(volumePercent(volume, maxVolumeValue)); } function muteVolume() { if (!sinkModel.defaultSink) { return; } var toMute = !sinkModel.defaultSink.muted; sinkModel.defaultSink.muted = toMute; - osd.show(toMute ? 0 : volumePercent(sinkModel.defaultSink.volume)); + osd.show(toMute ? 0 : volumePercent(sinkModel.defaultSink.volume, maxVolumeValue)); } function increaseMicrophoneVolume() { if (!sourceModel.defaultSource) { return; } var volume = bound(sourceModel.defaultSource.volume + volumeStep, 0, 65536); sourceModel.defaultSource.volume = volume; osd.showMicrophone(volumePercent(volume)); } function decreaseMicrophoneVolume() { if (!sourceModel.defaultSource) { return; } var volume = bound(sourceModel.defaultSource.volume - volumeStep, 0, 65536); 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; } 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 } 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/src/context.h b/src/context.h index 4fdda10..b53c64e 100644 --- a/src/context.h +++ b/src/context.h @@ -1,162 +1,165 @@ /* 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 . */ #ifndef CONTEXT_H #define CONTEXT_H #include #include #include #include #include #include #include #include "maps.h" #include "operation.h" namespace QPulseAudio { class Server; class Q_DECL_EXPORT Context : public QObject { Q_OBJECT public: Context(QObject *parent = nullptr); ~Context(); static Context *instance(); + static const qint64 NormalVolume = PA_VOLUME_NORM; + static const qint64 MinimalVolume = 0; + static const qint64 MaximalVolume = (PA_VOLUME_NORM / 100.0) * 150; + void ref(); void unref(); bool isValid() { return m_context && m_mainloop; } const SinkMap &sinks() const { return m_sinks; } const SinkInputMap &sinkInputs() const { return m_sinkInputs; } const SourceMap &sources() const { return m_sources; } const SourceOutputMap &sourceOutputs() const { return m_sourceOutputs; } const ClientMap &clients() const { return m_clients; } const CardMap &cards() const { return m_cards; } const StreamRestoreMap &streamRestores() const { return m_streamRestores; } Server *server() const { return m_server; } void subscribeCallback(pa_context *context, pa_subscription_event_type_t type, uint32_t index); void contextStateCallback(pa_context *context); void sinkCallback(const pa_sink_info *info); void sinkInputCallback(const pa_sink_input_info *info); void sourceCallback(const pa_source_info *info); void sourceOutputCallback(const pa_source_output_info *info); void clientCallback(const pa_client_info *info); void cardCallback(const pa_card_info *info); void streamRestoreCallback(const pa_ext_stream_restore_info *info); void serverCallback(const pa_server_info *info); void setCardProfile(quint32 index, const QString &profile); void setDefaultSink(const QString &name); void setDefaultSource(const QString &name); void streamRestoreWrite(const pa_ext_stream_restore_info *info); template void setGenericVolume(quint32 index, int channel, qint64 newVolume, pa_cvolume cVolume, PAFunction pa_set_volume) { - // TODO: overdrive - newVolume = qBound(0, newVolume, 65536); + newVolume = qBound(0, newVolume, PA_VOLUME_MAX); pa_cvolume newCVolume = cVolume; if (channel == -1) { // -1 all channels for (int i = 0; i < newCVolume.channels; ++i) { newCVolume.values[i] = newVolume; } } else { Q_ASSERT(newCVolume.channels > channel); newCVolume.values[channel] = newVolume; } if (!PAOperation(pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_set_volume failed"; return; } } template void setGenericMute(quint32 index, bool mute, PAFunction pa_set_mute) { if (!PAOperation(pa_set_mute(m_context, index, mute, nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_set_mute failed"; return; } } template void setGenericPort(quint32 index, const QString &portName, PAFunction pa_set_port) { if (!PAOperation(pa_set_port(m_context, index, portName.toUtf8().constData(), nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_set_port failed"; return; } } template void setGenericDeviceForStream(quint32 streamIndex, quint32 deviceIndex, PAFunction pa_move_stream_to_device) { if (!PAOperation(pa_move_stream_to_device(m_context, streamIndex, deviceIndex, nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_move_stream_to_device failed"; return; } } private: void connectToDaemon(); void reset(); // Don't forget to add things to reset(). SinkMap m_sinks; SinkInputMap m_sinkInputs; SourceMap m_sources; SourceOutputMap m_sourceOutputs; ClientMap m_clients; CardMap m_cards; StreamRestoreMap m_streamRestores; Server *m_server; pa_context *m_context; pa_glib_mainloop *m_mainloop; int m_references; static Context* s_context; }; } // QPulseAudio #endif // CONTEXT_H diff --git a/src/kcm/package/contents/ui/VolumeSlider.qml b/src/kcm/package/contents/ui/VolumeSlider.qml index d0c29ef..ee33532 100644 --- a/src/kcm/package/contents/ui/VolumeSlider.qml +++ b/src/kcm/package/contents/ui/VolumeSlider.qml @@ -1,91 +1,104 @@ /* 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.Layouts 1.0 import QtQuick.Controls 1.0 +import org.kde.plasma.private.volume 0.1 + RowLayout { + Layout.bottomMargin: hundredPercentLabel.height + 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: 0 - // FIXME: I do wonder if exposing max through the model would be useful at all - maximumValue: 65536 - stepSize: maximumValue / 100 + minimumValue: PulseAudio.MinimalVolume + maximumValue: PulseAudio.MaximalVolume visible: HasVolume enabled: VolumeWritable && !Muted onVolumeChanged: { ignoreValueChange = true; value = Volume; ignoreValueChange = false; } onValueChanged: { if (!ignoreValueChange) { Volume = value; 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(); } } + Label { + id: hundredPercentLabel + z: slider.z - 1 + x: (slider.width / slider.maximumValue) * PulseAudio.NormalVolume - width / 2 + y: slider.height / 1.2 + opacity: 0.5 + font.pixelSize: slider.height / 2.2 + text: i18n("100%") + } + Timer { id: updateTimer interval: 200 onTriggered: slider.value = Volume } } 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.floor(slider.value / slider.maximumValue * 100.0)) + 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/src/qml/plugin.cpp b/src/qml/plugin.cpp index 324a765..72a08a1 100644 --- a/src/qml/plugin.cpp +++ b/src/qml/plugin.cpp @@ -1,48 +1,61 @@ /* 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 "globalactioncollection.h" #include "volumeosd.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, "SourceOutputModel"); qmlRegisterType(uri, 0, 1, "StreamRestoreModel"); qmlRegisterType(uri, 0, 1, "GlobalAction"); qmlRegisterType(uri, 0, 1, "GlobalActionCollection"); qmlRegisterType(uri, 0, 1, "VolumeOSD"); + qmlRegisterSingletonType(uri, 0, 1, "PulseAudio", pulseaudio_singleton); qmlRegisterType(); qmlRegisterType(); qmlRegisterType(); }