diff --git a/components/mobileshell/CMakeLists.txt b/components/mobileshell/CMakeLists.txt index 263d716b..de291ae2 100644 --- a/components/mobileshell/CMakeLists.txt +++ b/components/mobileshell/CMakeLists.txt @@ -91,6 +91,7 @@ ecm_target_qml_sources(mobileshellplugin SOURCES qml/popups/volumeosd/VolumeChangedPopup.qml qml/popups/volumeosd/VolumeOSD.qml qml/popups/volumeosd/VolumeOSDProvider.qml + qml/popups/volumeosd/VolumeSlider.qml qml/popups/PopupProviderLoader.qml qml/statusbar/indicators/BatteryIndicator.qml diff --git a/components/mobileshell/qml/popups/volumeosd/AudioApplet.qml b/components/mobileshell/qml/popups/volumeosd/AudioApplet.qml index be79a791..8a8d50ef 100644 --- a/components/mobileshell/qml/popups/volumeosd/AudioApplet.qml +++ b/components/mobileshell/qml/popups/volumeosd/AudioApplet.qml @@ -27,53 +27,68 @@ ColumnLayout { property real scale: 1.0 - PulseObjectFilterModel { - id: paSinkFilterModel - sortRoleName: "SortByDefault" - sortOrder: Qt.DescendingOrder - filterOutInactiveDevices: true - sourceModel: MobileShell.AudioInfo.paSinkModel - } + // Input devices + readonly property SourceModel paSourceModel: SourceModel { id: paSourceModel } - SourceModel { - id: paSourceModel - } + // Output devices + readonly property SinkModel paSinkModel: SinkModel { id: paSinkModel } - PulseObjectFilterModel { + // Confusingly, Sink Input is what PulseAudio calls streams that send audio to an output device + readonly property SinkInputModel paSinkInputModel: SinkInputModel { id: paSinkInputModel } + + // Confusingly, Source Output is what PulseAudio calls streams that take audio from an input device + readonly property SourceOutputModel paSourceOutputModel: SourceOutputModel { id: paSourceOutputModel } + + // Active input devices + readonly property PulseObjectFilterModel paSourceFilterModel: PulseObjectFilterModel { id: paSourceFilterModel - sortRoleName: "SortByDefault" - sortOrder: Qt.DescendingOrder filterOutInactiveDevices: true + filterVirtualDevices: true sourceModel: paSourceModel } - CardModel { - id: paCardModel + // Active output devices + readonly property PulseObjectFilterModel paSinkFilterModel: PulseObjectFilterModel { + id: paSinkFilterModel + filterOutInactiveDevices: true + filterVirtualDevices: true + sourceModel: paSinkModel } - // ui elements + // non-virtual streams going to output devices + readonly property PulseObjectFilterModel paSinkInputFilterModel: PulseObjectFilterModel { + id: paSinkInputFilterModel + filters: [ + { role: "VirtualStream", value: false }, + { role: "Client", value: (client) => client.name !== "libcanberra" }, + ] + sourceModel: paSinkInputModel + } + + // non-virtual streams coming from input devices + readonly property PulseObjectFilterModel paSourceOutputFilterModel: PulseObjectFilterModel { + id: paSourceOutputFilterModel + filters: [ { role: "VirtualStream", value: false } ] + sourceModel: paSourceOutputModel + } + + readonly property CardModel paCardModel: CardModel { id: paCardModel } + + // UI elements PopupCard { Layout.alignment: Qt.AlignHCenter Layout.bottomMargin: Kirigami.Units.gridUnit - - transform: Scale { - origin.x: Math.round(implicitWidth / 2) - origin.y: Math.round(height / 2) - xScale: audioApplet.scale - yScale: audioApplet.scale - } + scaleFactor: audioApplet.scale contentItem: ColumnLayout { - anchors.rightMargin: Kirigami.Units.smallSpacing - anchors.leftMargin: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing Kirigami.Heading { level: 2 - text: i18n("Outputs") + text: i18n("Output Devices") + wrapMode: Text.Wrap Layout.fillWidth: true - Layout.topMargin: Kirigami.Units.smallSpacing - Layout.leftMargin: Kirigami.Units.smallSpacing } Repeater { @@ -83,9 +98,8 @@ ColumnLayout { model: paSinkFilterModel delegate: DeviceListItem { Layout.fillWidth: true - Layout.margins: Kirigami.Units.smallSpacing type: "sink" - onlyone: sinkView.count === 1 + onlyOne: sinkView.count === 1 } } } @@ -94,24 +108,16 @@ ColumnLayout { PopupCard { Layout.alignment: Qt.AlignHCenter Layout.bottomMargin: Kirigami.Units.gridUnit - - transform: Scale { - origin.x: Math.round(implicitWidth / 2) - origin.y: Math.round(height / 2) - xScale: audioApplet.scale - yScale: audioApplet.scale - } + scaleFactor: audioApplet.scale contentItem: ColumnLayout { - anchors.rightMargin: Kirigami.Units.smallSpacing - anchors.leftMargin: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing Kirigami.Heading { level: 2 - text: i18n("Inputs") + text: i18n("Input Devices") + wrapMode: Text.Wrap Layout.fillWidth: true - Layout.topMargin: Kirigami.Units.smallSpacing - Layout.leftMargin: Kirigami.Units.smallSpacing } Repeater { @@ -121,38 +127,30 @@ ColumnLayout { model: paSourceFilterModel delegate: DeviceListItem { Layout.fillWidth: true - Layout.margins: Kirigami.Units.smallSpacing type: "source" - onlyone: sinkView.count === 1 + onlyOne: sourceView.count === 1 } } } } PopupCard { - visible: sourceInputView.model.count + sourceMediaInputView.model.count !== 0 + visible: (sourceMediaInputView.count + sourceInputView.count) > 0 Layout.alignment: Qt.AlignHCenter Layout.bottomMargin: Kirigami.Units.gridUnit - - transform: Scale { - origin.x: Math.round(implicitWidth / 2) - origin.y: Math.round(height / 2) - xScale: audioApplet.scale - yScale: audioApplet.scale - } + scaleFactor: audioApplet.scale contentItem: ColumnLayout { - anchors.rightMargin: Kirigami.Units.smallSpacing - anchors.leftMargin: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing Kirigami.Heading { level: 2 text: i18n("Playback Streams") + wrapMode: Text.Wrap Layout.fillWidth: true - Layout.topMargin: Kirigami.Units.smallSpacing - Layout.leftMargin: Kirigami.Units.smallSpacing } + // "Grouped" media sources (ex. Notifications) Repeater { id: sourceMediaInputView Layout.fillWidth: true @@ -166,25 +164,22 @@ ColumnLayout { Layout.margins: Kirigami.Units.smallSpacing width: sourceOutputView.width type: "sink-input" - devicesModel: sourceView.model + devicesModel: paSinkFilterModel } } + // Regular playback streams Repeater { id: sourceInputView Layout.fillWidth: true - model: PulseObjectFilterModel { - filters: [ { role: "VirtualStream", value: false } ] - sourceModel: SinkInputModel {} - } + model: paSinkInputFilterModel delegate: StreamListItem { Layout.fillWidth: true - Layout.margins: Kirigami.Units.smallSpacing width: sourceOutputView.width type: "sink-input" - devicesModel: sourceView.model + devicesModel: paSinkFilterModel } } } @@ -194,40 +189,29 @@ ColumnLayout { visible: sourceOutputView.model.count !== 0 Layout.alignment: Qt.AlignHCenter Layout.bottomMargin: Kirigami.Units.gridUnit - - transform: Scale { - origin.x: Math.round(implicitWidth / 2) - origin.y: Math.round(height / 2) - xScale: audioApplet.scale - yScale: audioApplet.scale - } + scaleFactor: audioApplet.scale contentItem: ColumnLayout { - anchors.rightMargin: Kirigami.Units.smallSpacing - anchors.leftMargin: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing Kirigami.Heading { level: 2 text: i18n("Recording Streams") + wrapMode: Text.Wrap Layout.fillWidth: true - Layout.topMargin: Kirigami.Units.smallSpacing - Layout.leftMargin: Kirigami.Units.smallSpacing } Repeater { id: sourceOutputView Layout.fillWidth: true - model: PulseObjectFilterModel { - filters: [ { role: "VirtualStream", value: false } ] - sourceModel: SourceOutputModel {} - } + model: paSourceOutputFilterModel + delegate: StreamListItem { Layout.fillWidth: true - Layout.margins: Kirigami.Units.smallSpacing width: sourceOutputView.width type: "source-output" - devicesModel: sourceView.model + devicesModel: paSourceFilterModel } } } diff --git a/components/mobileshell/qml/popups/volumeosd/DeviceListItem.qml b/components/mobileshell/qml/popups/volumeosd/DeviceListItem.qml index 3a51f2f2..d296451b 100644 --- a/components/mobileshell/qml/popups/volumeosd/DeviceListItem.qml +++ b/components/mobileshell/qml/popups/volumeosd/DeviceListItem.qml @@ -11,25 +11,24 @@ import org.kde.plasma.private.volume 0.1 // adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/DeviceListItem.qml ListItemBase { - readonly property var currentPort: Ports[ActivePortIndex] - readonly property var currentActivePortIndex: ActivePortIndex - readonly property var currentMuted: Muted - readonly property var activePortIndex: ActivePortIndex - property bool onlyone: false + readonly property var currentPort: model.Ports[model.ActivePortIndex] + readonly property var currentActivePortIndex: model.ActivePortIndex + readonly property var currentMuted: model.Muted + readonly property var activePortIndex: model.ActivePortIndex label: { if (currentPort && currentPort.description) { - if (onlyone || !Description) { + if (onlyOne || !model.Description) { return currentPort.description; } else { - return i18nc("label of device items", "%1 (%2)", currentPort.description, Description); + return i18nc("label of device items", "%1 (%2)", currentPort.description, model.Description); } } - if (Description) { - return Description; + if (model.Description) { + return model.Description; } - if (Name) { - return Name; + if (model.Name) { + return model.Name; } return i18n("Device name not found"); } diff --git a/components/mobileshell/qml/popups/volumeosd/ListItemBase.qml b/components/mobileshell/qml/popups/volumeosd/ListItemBase.qml index 78ad0d26..e3d6d0ee 100644 --- a/components/mobileshell/qml/popups/volumeosd/ListItemBase.qml +++ b/components/mobileshell/qml/popups/volumeosd/ListItemBase.qml @@ -6,52 +6,57 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import QtQuick 2.15 -import QtQuick.Controls 2.15 as Controls -import QtQuick.Layouts 1.1 -import QtQuick.Window 2.2 +import QtQuick +import QtQuick.Controls as Controls +import QtQuick.Layouts -import org.kde.kirigami 2.20 as Kirigami -import org.kde.ksvg 1.0 as KSvg -import org.kde.kquickcontrolsaddons 2.0 +import org.kde.kirigami as Kirigami +import org.kde.ksvg as KSvg +import org.kde.kquickcontrolsaddons import org.kde.plasma.core as PlasmaCore -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.plasma.private.volume 0.1 +import org.kde.plasma.components as PlasmaComponents +import org.kde.plasma.extras as PlasmaExtras +import org.kde.plasma.private.volume import "icon.js" as Icon // adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/ListItemBase.qml -Controls.Control { +Controls.AbstractButton { id: baseItem property string label property alias listIcon: clientIcon.source property string type // sink, source, source-output - MouseArea { - id: clickArea - anchors.fill: parent - z: -1 - onClicked: { - if (selectButton.visible) { - model.PulseObject.default = true; - } - } + property bool onlyOne: false + + // Whether this item is selected + readonly property bool supportsSelection: (baseItem.type == "sink" || baseItem.type == "source") + readonly property bool selected: supportsSelection && (model.PulseObject.hasOwnProperty("default") ? model.PulseObject.default : false) + + onClicked: { + // Set as the default audio device + model.PulseObject.default = true + } + + topPadding: Kirigami.Units.mediumSpacing + bottomPadding: Kirigami.Units.mediumSpacing + leftPadding: Kirigami.Units.mediumSpacing + rightPadding: Kirigami.Units.mediumSpacing + + background: Rectangle { + radius: Kirigami.Units.cornerRadius + // border.width: 1 + // border.color: baseItem.selected ? Kirigami.Theme.highlightColor : 'transparent' + color: (baseItem.selected || baseItem.down) + ? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.3) + : 'transparent' } contentItem: RowLayout { id: row spacing: Kirigami.Units.smallSpacing - PlasmaComponents.RadioButton { - id: selectButton - Layout.alignment: Qt.AlignTop - Layout.topMargin: Math.round(row.height / 2 - implicitHeight - Kirigami.Units.smallSpacing / 2) // align with text - checked: model.PulseObject.hasOwnProperty("default") ? model.PulseObject.default : false - visible: (baseItem.type == "sink" && sinkView.model.count > 1) || (baseItem.type == "source" && sourceView.model.count > 1) - onClicked: model.PulseObject.default = true - } - // application icon Kirigami.Icon { id: clientIcon @@ -68,6 +73,24 @@ Controls.Control { } } + RowLayout { + spacing: 0 + Layout.maximumWidth: Infinity // Ignore maximum width of children + visible: (baseItem.type === "sink" || baseItem.type === "source") && !baseItem.onlyOne + + PlasmaComponents.RadioButton { + id: defaultButton + Accessible.ignored: true // read out from delegate + activeFocusOnTab: false // toggle from delegate + checked: model.PulseObject?.default ?? false + onToggled: { + if (checked) { + baseItem.click(); + } + } + } + } + ColumnLayout { Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true @@ -87,9 +110,10 @@ Controls.Control { } PlasmaComponents.ToolButton { + id: viewButton Layout.alignment: Qt.AlignBottom Layout.bottomMargin: -Kirigami.Units.smallSpacing - icon.name: "application-menu" + icon.name: "view-more-symbolic" checkable: true checked: contextMenu.visible && contextMenu.visualParent === this visible: contextMenu.hasContent @@ -132,94 +156,26 @@ Controls.Control { Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing - PlasmaComponents.ToolButton { - icon.name: Icon.name(Volume / PulseAudio.NormalVolume * 100.0, Muted) - text: Muted ? i18n("Unmute") : i18n("Mute") - display: Controls.AbstractButton.IconOnly - Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium - Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium - onClicked: { - Muted = !Muted - } - } - // this slider was effectively copied from the source (linked at the top of the file) - PlasmaComponents.Slider { + VolumeSlider { id: slider + Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - - // 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: true - readonly property bool forceRaiseMaxVolume: volume >= PulseAudio.NormalVolume * 1.01 - from: PulseAudio.MinimalVolume - to: PulseAudio.NormalVolume - stepSize: to / (to / PulseAudio.NormalVolume * 100.0) - - visible: model.HasVolume === true // (may be undefined) + to: model.Volume >= PulseAudio.NormalVolume * 1.01 ? PulseAudio.MaximalVolume : PulseAudio.NormalVolume + stepSize: PulseAudio.NormalVolume / 100.0 + property real myStepSize: PulseAudio.NormalVolume / 100.0 + visible: model.HasVolume !== false // Devices always have volume but Streams don't necessarily enabled: model.VolumeWritable - opacity: Muted ? 0.5 : 1 + muted: model.Muted + volumeObject: model.PulseObject + activeFocusOnTab: false // access from delegate - Accessible.name: i18nc("Accessibility data on volume slider", "Adjust volume for %1", baseItem.label) - - background: KSvg.FrameSvgItem { - imagePath: "widgets/slider" - prefix: "groove" - width: parent.availableWidth - height: margins.top + margins.bottom - anchors.centerIn: parent - scale: parent.mirrored ? -1 : 1 - - KSvg.FrameSvgItem { - imagePath: "widgets/slider" - prefix: "groove-highlight" - anchors.left: parent.left - y: (parent.height - height) / 2 - width: Math.max(margins.left + margins.right, slider.handle.x * meter.volume) - height: Math.max(margins.top + margins.bottom, parent.height) - opacity: meter.available && (meter.volume > 0 || animation.running) - VolumeMonitor { - id: meter - target: parent.visible ? model.PulseObject : null - } - Behavior on width { - NumberAnimation { - id: animation - duration: Kirigami.Units.shortDuration - easing.type: Easing.OutQuad - } - } - } + value: to, model.Volume + onMoved: { + model.Volume = value; + model.Muted = value === 0; } - - Component.onCompleted: { - ignoreValueChange = false; - } - - onVolumeChanged: { - var oldIgnoreValueChange = ignoreValueChange; - ignoreValueChange = true; - value = Volume; - ignoreValueChange = oldIgnoreValueChange; - } - - onValueChanged: { - if (!ignoreValueChange) { - Volume = value; - Muted = value == 0; - - if (!pressed) { - updateTimer.restart(); - } - } - } - onPressedChanged: { if (!pressed) { // Make sure to sync the volume once the button was @@ -227,14 +183,28 @@ Controls.Control { // 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(); + value = Qt.binding(() => model.Volume); } } - Timer { - id: updateTimer - interval: 200 - onTriggered: slider.value = Volume + function updateVolume() { + if (model.Volume > PulseAudio.NormalVolume) { + model.Volume = PulseAudio.NormalVolume; + } + } + + SequentialAnimation { + id: seqAnimation + NumberAnimation { + id: toAnimation + target: slider + property: "to" + duration: Kirigami.Units.shortDuration + easing.type: Easing.InOutQuad + } + ScriptAction { + script: slider.updateVolume() + } } } PlasmaComponents.Label { @@ -264,8 +234,4 @@ Controls.Control { } } } - - function setVolumeByPercent(targetPercent) { - model.PulseObject.volume = Math.round(PulseAudio.NormalVolume * (targetPercent/100)); - } } diff --git a/components/mobileshell/qml/popups/volumeosd/PopupCard.qml b/components/mobileshell/qml/popups/volumeosd/PopupCard.qml index 32c1bd6d..fe997707 100644 --- a/components/mobileshell/qml/popups/volumeosd/PopupCard.qml +++ b/components/mobileshell/qml/popups/volumeosd/PopupCard.qml @@ -18,13 +18,21 @@ import org.kde.ksvg 1.0 as KSvg import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.private.mobileshell as MobileShell -// capture presses on the audio applet so it doesn't close the overlay Controls.Control { id: content - implicitWidth: Math.min(Kirigami.Units.gridUnit * 20, Screen.width - Kirigami.Units.gridUnit * 2) - padding: Kirigami.Units.smallSpacing * 2 property bool popupBackground: false + property real scaleFactor: 1.0 + + implicitWidth: Math.min(Kirigami.Units.gridUnit * 20, Screen.width - Kirigami.Units.gridUnit * 2) + padding: Kirigami.Units.largeSpacing + + transform: Scale { + origin.x: Math.round(implicitWidth / 2) + origin.y: Math.round(height / 2) + xScale: scaleFactor + yScale: scaleFactor + } Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.inherit: false diff --git a/components/mobileshell/qml/popups/volumeosd/VolumeChangedPopup.qml b/components/mobileshell/qml/popups/volumeosd/VolumeChangedPopup.qml index c6278913..26503c3a 100644 --- a/components/mobileshell/qml/popups/volumeosd/VolumeChangedPopup.qml +++ b/components/mobileshell/qml/popups/volumeosd/VolumeChangedPopup.qml @@ -44,7 +44,7 @@ Window { if (cards.state == "closed") { hideTimer.stop(); window.open(); - } else if (!volumeSlider.isPressed) { + } else if (!volumeSlider.pressed) { hideTimer.restart(); } } @@ -154,81 +154,41 @@ Window { property int volumePercent: PreferredDevice.sink.volume / PulseAudio.NormalVolume * 100.0 PlasmaComponents.ToolButton { - icon.name: !PreferredDevice.sink || PreferredDevice.sink.muted ? "audio-volume-muted" : MobileShell.AudioInfo.icon - text: !PreferredDevice.sink || PreferredDevice.sink.muted ? i18n("Unmute") : i18n("Mute") + icon.name: !PreferredDevice.sink || (PreferredDevice.sink.muted ? "audio-volume-muted" : MobileShell.AudioInfo.icon) + text: !PreferredDevice.sink || (PreferredDevice.sink.muted ? i18n("Unmute") : i18n("Mute")) display: Controls.AbstractButton.IconOnly Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: Kirigami.Units.iconSizes.medium Layout.preferredHeight: Kirigami.Units.iconSizes.medium Layout.rightMargin: Kirigami.Units.smallSpacing + onClicked: { hideTimer.restart(); PreferredDevice.sink.muted = !PreferredDevice.sink.muted; } } - PlasmaComponents.Slider { + VolumeSlider { id: volumeSlider Layout.fillWidth: true Layout.fillHeight: true Layout.alignment: Qt.AlignVCenter Layout.rightMargin: Kirigami.Units.smallSpacing * 2 - property real volume: PreferredDevice.sink.volume - property bool muted: PreferredDevice.sink.muted - property bool ignoreValueChange: false - property bool isPressed: false - from: PulseAudio.MinimalVolume to: PulseAudio.NormalVolume stepSize: to / (to / PulseAudio.NormalVolume * 100.0) - opacity: muted ? 0.5 : 1.0 - Component.onCompleted: { - ignoreValueChange = false; + volumeObject: PreferredDevice.sink + muted: PreferredDevice.sink.muted + value: PreferredDevice.sink.volume + + onMoved: { + PreferredDevice.sink.volume = value; + PreferredDevice.sink.muted = value === 0; } - - onVolumeChanged: { - if (!window.visible) { - return; - } - var oldIgnoreValueChange = ignoreValueChange; - ignoreValueChange = true; - value = muted ? 0 : PreferredDevice.sink.volume; - ignoreValueChange = oldIgnoreValueChange; - if (volumeSlider.isPressed) { - return; - } - window.open(); - hideTimer.restart(); - } - - onMutedChanged: { - var oldIgnoreValueChange = ignoreValueChange; - ignoreValueChange = true; - value = muted ? 0 : PreferredDevice.sink.volume; - ignoreValueChange = oldIgnoreValueChange; - if (!window.visible || volumeSlider.isPressed) { - return; - } - window.open(); - hideTimer.restart(); - } - - onValueChanged: { - if (!ignoreValueChange) { - PreferredDevice.sink.muted = false; - PreferredDevice.sink.volume = value; - if (!volumeSlider.isPressed) { - updateTimer.restart(); - } - } - } - onPressedChanged: { - volumeSlider.isPressed = pressed; if (pressed) { - window.open(); hideTimer.stop(); } else { // Make sure to sync the volume once the button was @@ -236,16 +196,10 @@ Window { // Otherwise it might be that the slider is at v10 // whereas PA rejected the volume change and is // still at v15 (e.g.). + value = Qt.binding(() => PreferredDevice.sink.volume); hideTimer.restart(); - updateTimer.restart(); } } - - Timer { - id: updateTimer - interval: 200 - onTriggered: volumeSlider.value = PreferredDevice.sink.volume - } } PlasmaComponents.ToolButton { diff --git a/components/mobileshell/qml/popups/volumeosd/VolumeOSD.qml b/components/mobileshell/qml/popups/volumeosd/VolumeOSD.qml index 8a7af2c3..70039807 100644 --- a/components/mobileshell/qml/popups/volumeosd/VolumeOSD.qml +++ b/components/mobileshell/qml/popups/volumeosd/VolumeOSD.qml @@ -177,44 +177,32 @@ Window { Layout.alignment: Qt.AlignHCenter Layout.bottomMargin: Kirigami.Units.gridUnit - transform: Scale { - origin.x: Math.round(implicitWidth / 2) - origin.y: Math.round(height / 2) - xScale: flickable.scale - yScale: flickable.scale - } + scaleFactor: flickable.scale - contentItem: RowLayout { + contentItem: PlasmaComponents.ToolButton { + id: audioSettingsButton - PlasmaComponents.ToolButton { - property int addedPadding: Kirigami.Units.smallSpacing * 2 - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - Layout.preferredWidth: parent.width - addedPadding * 2 - Layout.preferredHeight: Kirigami.Units.iconSizes.medium - Layout.margins: addedPadding - - contentItem: Item { - anchors.fill: parent - RowLayout { - spacing: Kirigami.Units.largeSpacing - anchors.centerIn: parent - Kirigami.Icon { - Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium - Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium - source: "settings-configure" - } - PlasmaComponents.Label { - text: i18n("Open audio settings") - anchors.verticalCenter: parent.verticalCenter - } + contentItem: Item { + anchors.fill: parent + RowLayout { + spacing: Kirigami.Units.largeSpacing + anchors.centerIn: parent + Kirigami.Icon { + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium + Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium + source: "settings-configure" + } + PlasmaComponents.Label { + text: i18n("Open audio settings") + anchors.verticalCenter: parent.verticalCenter } } + } - onClicked: { - MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_pulseaudio"); - window.close(); - } + onClicked: { + MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_pulseaudio"); + window.close(); } } } diff --git a/components/mobileshell/qml/popups/volumeosd/VolumeSlider.qml b/components/mobileshell/qml/popups/volumeosd/VolumeSlider.qml new file mode 100644 index 00000000..f0edc0e5 --- /dev/null +++ b/components/mobileshell/qml/popups/volumeosd/VolumeSlider.qml @@ -0,0 +1,131 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Harald Sitter + SPDX-FileCopyrightText: 2019 Sefa Eyeoglu + SPDX-FileCopyrightText: 2022 ivan (@ratijas) tkachenko + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +import QtQuick +import QtQuick.Layouts + +import org.kde.kquickcontrolsaddons +import org.kde.plasma.components as PC3 +import org.kde.ksvg as KSvg +import org.kde.kirigami as Kirigami +import org.kde.plasma.private.volume + +// Audio volume slider. Value represents desired volume level in +// device-specific units, while volume property reports current volume level +// normalized to 0..1 range. +PC3.Slider { + id: control + + property VolumeObject volumeObject + + // When muted, the whole slider will appear slightly faded, but remain + // functional and interactive. + property bool muted: false + + // Current (monitored) volume. To be animated. Do not update too fast + // (i.e. faster or close to screen refresh rate), otherwise it won't + // animate smoothly. + property real volume: meter.volume + + VolumeMonitor { + id: meter + target: control.visible ? control.volumeObject : null + } + + Behavior on volume { + NumberAnimation { + id: animate + duration: Kirigami.Units.shortDuration + easing.type: Easing.OutQuad + } + } + + // When a maximum volume limit is raised/lower, animate the change. + Behavior on to { + NumberAnimation { + duration: Kirigami.Units.shortDuration + easing.type: Easing.InOutQuad + } + } + + opacity: muted ? 0.5 : 1 + // Prevents the groove from showing through the handle + layer.enabled: opacity < 1 + + wheelEnabled: false + // `wheelEnabled: true` doesn't work we can't both respect stepsize + // on scroll and allow fine-tuning on drag. + // So we have to implement the scroll handling ourselves. See + // https://bugreports.qt.io/browse/QTBUG-93081 + WheelHandler { + orientation: Qt.Vertical | Qt.Horizontal + property int wheelDelta: 0 + acceptedButtons: Qt.NoButton + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: wheel => { + const lastValue = control.value + // We want a positive delta to increase the slider for up/right scrolling, + // independently of the scrolling inversion setting + // The x-axis is also inverted (scrolling right produce negative values) + const delta = (wheel.angleDelta.y || -wheel.angleDelta.x) * (wheel.inverted ? -1 : 1) + wheelDelta += delta; + // magic number 120 for common "one click" + // See: https://doc.qt.io/qt-6/qml-qtquick-wheelevent.html#angleDelta-prop + while (wheelDelta >= 120) { + wheelDelta -= 120; + control.increase(); + } + while (wheelDelta <= -120) { + wheelDelta += 120; + control.decrease(); + } + if (lastValue !== control.value) { + control.moved(); + } + } + } + + background: KSvg.FrameSvgItem { + imagePath: "widgets/slider" + prefix: "groove" + + implicitWidth: control.horizontal ? Kirigami.Units.gridUnit * 12 : fixedMargins.left + fixedMargins.right + implicitHeight: control.vertical ? Kirigami.Units.gridUnit * 12 : fixedMargins.top + fixedMargins.bottom + + width: control.horizontal ? Math.max(fixedMargins.left + fixedMargins.right, control.availableWidth) : implicitWidth + height: control.vertical ? Math.max(fixedMargins.top + fixedMargins.bottom, control.availableHeight) : implicitHeight + + x: control.leftPadding + (control.horizontal ? 0 : Math.round((control.availableWidth - width) / 2)) + y: control.topPadding + (control.vertical ? 0 : Math.round((control.availableHeight - height) / 2)) + + KSvg.FrameSvgItem { + imagePath: "widgets/slider" + prefix: "groove-highlight" + + anchors.left: parent.left + anchors.bottom: parent.bottom + LayoutMirroring.enabled: control.mirrored + + width: control.horizontal ? Math.max(fixedMargins.left + fixedMargins.right, Math.round(control.position * (control.availableWidth - control.handle.width / 2) + (control.handle.width / 2))) : parent.width + height: control.vertical ? Math.max(fixedMargins.top + fixedMargins.bottom, Math.round(control.position * (control.availableHeight - control.handle.height / 2) + (control.handle.height / 2))) : parent.height + } + + KSvg.FrameSvgItem { + imagePath: "widgets/slider" + prefix: "groove-highlight" + status: KSvg.FrameSvgItem.Selected + visible: meter.available && control.volume > 0 + + anchors.left: parent.left + anchors.bottom: parent.bottom + LayoutMirroring.enabled: control.mirrored + + width: control.horizontal ? Math.max(fixedMargins.left + fixedMargins.right, Math.round(control.volume * control.position * control.availableWidth)) : parent.width + height: control.vertical ? Math.max(fixedMargins.top + fixedMargins.bottom, Math.round(control.volume * control.position * control.availableHeight)) : parent.height + } + } +}