From dd3e366e175612398d1394088291bef98a9971aa Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Sun, 31 May 2026 14:06:23 +0200 Subject: [PATCH] Harden quick settings and volume OSD models Clamp quick settings page math to valid bounds and guard volume OSD\ncontrols when PulseAudio objects are absent. Remove unused delegate\nrequired properties tied to enabled state. --- .../actiondrawer/private/QuickSettings.qml | 21 +++++++++-------- .../private/QuickSettingsDelegate.qml | 1 - .../private/QuickSettingsStatusRow.qml | 1 - .../qml/popups/volumeosd/ListItemBase.qml | 13 ++++++++--- .../qml/popups/volumeosd/StreamListItem.qml | 1 + .../popups/volumeosd/VolumeChangedPopup.qml | 23 +++++++++++++------ .../qml/popups/volumeosd/VolumeOSD.qml | 2 +- .../quicksettingsplugin/paginatemodel.cpp | 2 +- 8 files changed, 40 insertions(+), 24 deletions(-) diff --git a/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml b/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml index e9d44909..6a5385a9 100644 --- a/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml +++ b/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml @@ -29,12 +29,12 @@ Item { required property QS.QuickSettingsModel quickSettingsModel - readonly property real columns: Math.round(Math.min(6, Math.max(ShellSettings.Settings.quickSettingsColumns, width / intendedColumnWidth))) - readonly property real columnWidth: Math.floor(width / columns) - readonly property int minimizedColumns: Math.round(Math.min(8, Math.max(5, width / intendedMinimizedColumnWidth))) - readonly property real minimizedColumnWidth: Math.floor(width / minimizedColumns) + readonly property int columns: Math.max(1, Math.round(Math.min(6, Math.max(ShellSettings.Settings.quickSettingsColumns, width / intendedColumnWidth)))) + readonly property real columnWidth: Math.max(1, Math.floor(width / columns)) + readonly property int minimizedColumns: Math.max(1, Math.round(Math.min(8, Math.max(5, width / intendedMinimizedColumnWidth)))) + readonly property real minimizedColumnWidth: Math.max(1, Math.floor(width / minimizedColumns)) - readonly property real rowHeight: columnWidth * 0.7 + readonly property real rowHeight: Math.max(1, columnWidth * 0.7) readonly property real fullHeight: fullView.implicitHeight readonly property real intendedColumnWidth: Kirigami.Units.gridUnit * 7 @@ -43,16 +43,17 @@ Item { property real fullViewProgress: 1 - readonly property int columnCount: Math.floor(width/columnWidth) + readonly property int columnCount: Math.max(1, Math.floor(width / columnWidth)) readonly property int rowCount: { let totalRows = Math.ceil(quickSettingsCount / columnCount); let maxRows = root.isConvergence ? 3 : 5; // more than 5 is just disorienting - let targetRows = Math.floor(Window.height * (root.isConvergence ? 0.42 : 0.65) / rowHeight); + let targetRows = Math.max(1, Math.floor(Window.height * (root.isConvergence ? 0.42 : 0.65) / rowHeight)); return Math.max(1, Math.min(maxRows, Math.min(totalRows, targetRows))); } - readonly property int pageSize: rowCount * columnCount - readonly property int quickSettingsCount: quickSettingsModel.count + readonly property int pageSize: Math.max(1, rowCount * columnCount) + readonly property int quickSettingsCount: Math.max(0, quickSettingsModel.count) + readonly property int pageCount: Math.max(1, Math.ceil(quickSettingsCount / pageSize)) // Management tiles — promoted to full-width status rows in convergence. readonly property var __managementCommands: ({ @@ -279,7 +280,7 @@ Item { Layout.maximumHeight: visible ? rowCount * rowHeight : 0 Repeater { - model: Math.ceil(quickSettingsCount / pageSize) + model: root.isConvergence ? 0 : root.pageCount delegate: Flow { id: flow spacing: 0 diff --git a/components/mobileshell/qml/actiondrawer/private/QuickSettingsDelegate.qml b/components/mobileshell/qml/actiondrawer/private/QuickSettingsDelegate.qml index 908da0a7..6bcfecf7 100644 --- a/components/mobileshell/qml/actiondrawer/private/QuickSettingsDelegate.qml +++ b/components/mobileshell/qml/actiondrawer/private/QuickSettingsDelegate.qml @@ -26,7 +26,6 @@ MobileShell.BaseItem { required property string text required property string status required property string icon - required property bool enabled required property string settingsCommand required property var toggleFunction diff --git a/components/mobileshell/qml/actiondrawer/private/QuickSettingsStatusRow.qml b/components/mobileshell/qml/actiondrawer/private/QuickSettingsStatusRow.qml index 887afbf0..0729f8dc 100644 --- a/components/mobileshell/qml/actiondrawer/private/QuickSettingsStatusRow.qml +++ b/components/mobileshell/qml/actiondrawer/private/QuickSettingsStatusRow.qml @@ -18,7 +18,6 @@ Item { required property string text required property string status required property string icon - required property bool enabled required property var toggleFunction property bool compact: false diff --git a/components/mobileshell/qml/popups/volumeosd/ListItemBase.qml b/components/mobileshell/qml/popups/volumeosd/ListItemBase.qml index 240f91e6..bf6b94ff 100644 --- a/components/mobileshell/qml/popups/volumeosd/ListItemBase.qml +++ b/components/mobileshell/qml/popups/volumeosd/ListItemBase.qml @@ -32,12 +32,19 @@ Controls.AbstractButton { property string type // sink, source, source-output property bool onlyOne: false + property bool useVolumeObject: true // 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) + readonly property bool hasPulseObject: model.PulseObject !== null && model.PulseObject !== undefined + readonly property bool hasVolumeObject: useVolumeObject && hasPulseObject + readonly property bool selected: supportsSelection && hasPulseObject && (model.PulseObject.hasOwnProperty("default") ? model.PulseObject.default : false) onClicked: { + if (!hasPulseObject || !model.PulseObject.hasOwnProperty("default")) { + return; + } + // Set as the default audio device model.PulseObject.default = true } @@ -175,7 +182,7 @@ Controls.AbstractButton { visible: model.HasVolume !== false // Devices always have volume but Streams don't necessarily enabled: model.VolumeWritable muted: model.Muted - volumeObject: model.PulseObject + volumeObject: baseItem.hasVolumeObject ? model.PulseObject : null activeFocusOnTab: false // access from delegate value: to, model.Volume @@ -215,7 +222,7 @@ Controls.AbstractButton { } PlasmaComponents.Label { id: percentText - readonly property real value: model.PulseObject.volume > slider.to ? model.PulseObject.volume : slider.value + readonly property real value: baseItem.hasVolumeObject && model.PulseObject.volume > slider.to ? model.PulseObject.volume : slider.value readonly property real displayValue: Math.round(value / PulseAudio.NormalVolume * 100.0) Layout.alignment: Qt.AlignHCenter Layout.minimumWidth: percentMetrics.advanceWidth diff --git a/components/mobileshell/qml/popups/volumeosd/StreamListItem.qml b/components/mobileshell/qml/popups/volumeosd/StreamListItem.qml index f066e6c4..1d87807e 100644 --- a/components/mobileshell/qml/popups/volumeosd/StreamListItem.qml +++ b/components/mobileshell/qml/popups/volumeosd/StreamListItem.qml @@ -15,6 +15,7 @@ ListItemBase { property QtObject devicesModel readonly property bool isEventStream: Name == "sink-input-by-media-role:event" + useVolumeObject: !isEventStream label: { if (isEventStream) { diff --git a/components/mobileshell/qml/popups/volumeosd/VolumeChangedPopup.qml b/components/mobileshell/qml/popups/volumeosd/VolumeChangedPopup.qml index ef052ec8..0998856a 100644 --- a/components/mobileshell/qml/popups/volumeosd/VolumeChangedPopup.qml +++ b/components/mobileshell/qml/popups/volumeosd/VolumeChangedPopup.qml @@ -157,11 +157,13 @@ Window { anchors.leftMargin: Kirigami.Units.smallSpacing * 2 anchors.rightMargin: Kirigami.Units.smallSpacing - property int volumePercent: PreferredDevice.sink.volume / PulseAudio.NormalVolume * 100.0 + readonly property bool hasSink: PreferredDevice.sink !== null + property int volumePercent: hasSink ? PreferredDevice.sink.volume / PulseAudio.NormalVolume * 100.0 : 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")) + enabled: containerLayout.hasSink + icon.name: containerLayout.hasSink ? (PreferredDevice.sink.muted ? "audio-volume-muted" : MobileShell.AudioInfo.icon) : "audio-volume-muted" + text: containerLayout.hasSink && PreferredDevice.sink.muted ? i18n("Unmute") : i18n("Mute") display: Controls.AbstractButton.IconOnly Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: Kirigami.Units.iconSizes.medium @@ -169,6 +171,9 @@ Window { Layout.rightMargin: Kirigami.Units.smallSpacing onClicked: { + if (!containerLayout.hasSink) { + return; + } hideTimer.restart(); PreferredDevice.sink.muted = !PreferredDevice.sink.muted; } @@ -185,11 +190,15 @@ Window { to: PulseAudio.NormalVolume stepSize: to / (to / PulseAudio.NormalVolume * 100.0) - volumeObject: PreferredDevice.sink - muted: PreferredDevice.sink.muted - value: PreferredDevice.sink.volume + enabled: containerLayout.hasSink + volumeObject: containerLayout.hasSink ? PreferredDevice.sink : null + muted: containerLayout.hasSink ? PreferredDevice.sink.muted : false + value: containerLayout.hasSink ? PreferredDevice.sink.volume : PulseAudio.MinimalVolume onMoved: { + if (!containerLayout.hasSink) { + return; + } PreferredDevice.sink.volume = value; PreferredDevice.sink.muted = value === 0; } @@ -202,7 +211,7 @@ 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); + value = Qt.binding(() => containerLayout.hasSink ? PreferredDevice.sink.volume : PulseAudio.MinimalVolume); hideTimer.restart(); } } diff --git a/components/mobileshell/qml/popups/volumeosd/VolumeOSD.qml b/components/mobileshell/qml/popups/volumeosd/VolumeOSD.qml index c07a0e65..79ccdb61 100644 --- a/components/mobileshell/qml/popups/volumeosd/VolumeOSD.qml +++ b/components/mobileshell/qml/popups/volumeosd/VolumeOSD.qml @@ -201,7 +201,7 @@ Window { } PlasmaComponents.Label { text: i18n("Open audio settings") - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter } } } diff --git a/components/quicksettingsplugin/paginatemodel.cpp b/components/quicksettingsplugin/paginatemodel.cpp index 704ce29c..6cda643d 100644 --- a/components/quicksettingsplugin/paginatemodel.cpp +++ b/components/quicksettingsplugin/paginatemodel.cpp @@ -119,7 +119,7 @@ QHash PaginateModel::roleNames() const int PaginateModel::rowsByPageSize(int size) const { - return d->m_hasStaticRowCount ? size : !d->m_sourceModel ? 0 : qMin(d->m_sourceModel->rowCount() - d->m_firstItem, size); + return d->m_hasStaticRowCount ? qMax(size, 0) : !d->m_sourceModel ? 0 : qMax(qMin(d->m_sourceModel->rowCount() - d->m_firstItem, size), 0); } int PaginateModel::rowCount(const QModelIndex &parent) const