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.
This commit is contained in:
Marco Allegretti 2026-05-31 14:06:23 +02:00
parent 9a3db42f74
commit dd3e366e17
8 changed files with 40 additions and 24 deletions

View file

@ -29,12 +29,12 @@ Item {
required property QS.QuickSettingsModel quickSettingsModel required property QS.QuickSettingsModel quickSettingsModel
readonly property real columns: Math.round(Math.min(6, Math.max(ShellSettings.Settings.quickSettingsColumns, width / intendedColumnWidth))) readonly property int columns: Math.max(1, Math.round(Math.min(6, Math.max(ShellSettings.Settings.quickSettingsColumns, width / intendedColumnWidth))))
readonly property real columnWidth: Math.floor(width / columns) readonly property real columnWidth: Math.max(1, Math.floor(width / columns))
readonly property int minimizedColumns: Math.round(Math.min(8, Math.max(5, width / intendedMinimizedColumnWidth))) readonly property int minimizedColumns: Math.max(1, Math.round(Math.min(8, Math.max(5, width / intendedMinimizedColumnWidth))))
readonly property real minimizedColumnWidth: Math.floor(width / minimizedColumns) 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 fullHeight: fullView.implicitHeight
readonly property real intendedColumnWidth: Kirigami.Units.gridUnit * 7 readonly property real intendedColumnWidth: Kirigami.Units.gridUnit * 7
@ -43,16 +43,17 @@ Item {
property real fullViewProgress: 1 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: { readonly property int rowCount: {
let totalRows = Math.ceil(quickSettingsCount / columnCount); let totalRows = Math.ceil(quickSettingsCount / columnCount);
let maxRows = root.isConvergence ? 3 : 5; // more than 5 is just disorienting 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))); return Math.max(1, Math.min(maxRows, Math.min(totalRows, targetRows)));
} }
readonly property int pageSize: rowCount * columnCount readonly property int pageSize: Math.max(1, rowCount * columnCount)
readonly property int quickSettingsCount: quickSettingsModel.count 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. // Management tiles promoted to full-width status rows in convergence.
readonly property var __managementCommands: ({ readonly property var __managementCommands: ({
@ -279,7 +280,7 @@ Item {
Layout.maximumHeight: visible ? rowCount * rowHeight : 0 Layout.maximumHeight: visible ? rowCount * rowHeight : 0
Repeater { Repeater {
model: Math.ceil(quickSettingsCount / pageSize) model: root.isConvergence ? 0 : root.pageCount
delegate: Flow { delegate: Flow {
id: flow id: flow
spacing: 0 spacing: 0

View file

@ -26,7 +26,6 @@ MobileShell.BaseItem {
required property string text required property string text
required property string status required property string status
required property string icon required property string icon
required property bool enabled
required property string settingsCommand required property string settingsCommand
required property var toggleFunction required property var toggleFunction

View file

@ -18,7 +18,6 @@ Item {
required property string text required property string text
required property string status required property string status
required property string icon required property string icon
required property bool enabled
required property var toggleFunction required property var toggleFunction
property bool compact: false property bool compact: false

View file

@ -32,12 +32,19 @@ Controls.AbstractButton {
property string type // sink, source, source-output property string type // sink, source, source-output
property bool onlyOne: false property bool onlyOne: false
property bool useVolumeObject: true
// Whether this item is selected // Whether this item is selected
readonly property bool supportsSelection: (baseItem.type == "sink" || baseItem.type == "source") 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: { onClicked: {
if (!hasPulseObject || !model.PulseObject.hasOwnProperty("default")) {
return;
}
// Set as the default audio device // Set as the default audio device
model.PulseObject.default = true model.PulseObject.default = true
} }
@ -175,7 +182,7 @@ Controls.AbstractButton {
visible: model.HasVolume !== false // Devices always have volume but Streams don't necessarily visible: model.HasVolume !== false // Devices always have volume but Streams don't necessarily
enabled: model.VolumeWritable enabled: model.VolumeWritable
muted: model.Muted muted: model.Muted
volumeObject: model.PulseObject volumeObject: baseItem.hasVolumeObject ? model.PulseObject : null
activeFocusOnTab: false // access from delegate activeFocusOnTab: false // access from delegate
value: to, model.Volume value: to, model.Volume
@ -215,7 +222,7 @@ Controls.AbstractButton {
} }
PlasmaComponents.Label { PlasmaComponents.Label {
id: percentText 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) readonly property real displayValue: Math.round(value / PulseAudio.NormalVolume * 100.0)
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.minimumWidth: percentMetrics.advanceWidth Layout.minimumWidth: percentMetrics.advanceWidth

View file

@ -15,6 +15,7 @@ ListItemBase {
property QtObject devicesModel property QtObject devicesModel
readonly property bool isEventStream: Name == "sink-input-by-media-role:event" readonly property bool isEventStream: Name == "sink-input-by-media-role:event"
useVolumeObject: !isEventStream
label: { label: {
if (isEventStream) { if (isEventStream) {

View file

@ -157,11 +157,13 @@ Window {
anchors.leftMargin: Kirigami.Units.smallSpacing * 2 anchors.leftMargin: Kirigami.Units.smallSpacing * 2
anchors.rightMargin: Kirigami.Units.smallSpacing 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 { PlasmaComponents.ToolButton {
icon.name: !PreferredDevice.sink || (PreferredDevice.sink.muted ? "audio-volume-muted" : MobileShell.AudioInfo.icon) enabled: containerLayout.hasSink
text: !PreferredDevice.sink || (PreferredDevice.sink.muted ? i18n("Unmute") : i18n("Mute")) 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 display: Controls.AbstractButton.IconOnly
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Kirigami.Units.iconSizes.medium Layout.preferredWidth: Kirigami.Units.iconSizes.medium
@ -169,6 +171,9 @@ Window {
Layout.rightMargin: Kirigami.Units.smallSpacing Layout.rightMargin: Kirigami.Units.smallSpacing
onClicked: { onClicked: {
if (!containerLayout.hasSink) {
return;
}
hideTimer.restart(); hideTimer.restart();
PreferredDevice.sink.muted = !PreferredDevice.sink.muted; PreferredDevice.sink.muted = !PreferredDevice.sink.muted;
} }
@ -185,11 +190,15 @@ Window {
to: PulseAudio.NormalVolume to: PulseAudio.NormalVolume
stepSize: to / (to / PulseAudio.NormalVolume * 100.0) stepSize: to / (to / PulseAudio.NormalVolume * 100.0)
volumeObject: PreferredDevice.sink enabled: containerLayout.hasSink
muted: PreferredDevice.sink.muted volumeObject: containerLayout.hasSink ? PreferredDevice.sink : null
value: PreferredDevice.sink.volume muted: containerLayout.hasSink ? PreferredDevice.sink.muted : false
value: containerLayout.hasSink ? PreferredDevice.sink.volume : PulseAudio.MinimalVolume
onMoved: { onMoved: {
if (!containerLayout.hasSink) {
return;
}
PreferredDevice.sink.volume = value; PreferredDevice.sink.volume = value;
PreferredDevice.sink.muted = value === 0; PreferredDevice.sink.muted = value === 0;
} }
@ -202,7 +211,7 @@ Window {
// Otherwise it might be that the slider is at v10 // Otherwise it might be that the slider is at v10
// whereas PA rejected the volume change and is // whereas PA rejected the volume change and is
// still at v15 (e.g.). // still at v15 (e.g.).
value = Qt.binding(() => PreferredDevice.sink.volume); value = Qt.binding(() => containerLayout.hasSink ? PreferredDevice.sink.volume : PulseAudio.MinimalVolume);
hideTimer.restart(); hideTimer.restart();
} }
} }

View file

@ -201,7 +201,7 @@ Window {
} }
PlasmaComponents.Label { PlasmaComponents.Label {
text: i18n("Open audio settings") text: i18n("Open audio settings")
anchors.verticalCenter: parent.verticalCenter Layout.alignment: Qt.AlignVCenter
} }
} }
} }

View file

@ -119,7 +119,7 @@ QHash<int, QByteArray> PaginateModel::roleNames() const
int PaginateModel::rowsByPageSize(int size) 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 int PaginateModel::rowCount(const QModelIndex &parent) const