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
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

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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();
}
}

View file

@ -201,7 +201,7 @@ Window {
}
PlasmaComponents.Label {
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
{
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