merge: integrate the Appearance theme fixes (options boxed under their pickers, adjustable Wallpaper blur) into pre-release

cc
This commit is contained in:
vicrodh committed 2026-09-14 13:08:50 -06:00
commit 5042068fc3
18 files changed
+547 -103

No files matched your search

+2
View File
@@ -89,6 +89,7 @@ on:
- "scripts/test-array-model-qml.sh"
- "scripts/test-keyed-model-qml.sh"
- "scripts/test-multi-select-qml.sh"
- "scripts/test-settings-fieldset-qml.sh"
- "scripts/test-wallpaper-field-qml.sh"
- "scripts/test-card-menu-qml.sh"
- "scripts/test-orbit-daemon.py"
@@ -122,6 +123,7 @@ on:
- "scripts/test-array-model-qml.sh"
- "scripts/test-keyed-model-qml.sh"
- "scripts/test-multi-select-qml.sh"
- "scripts/test-settings-fieldset-qml.sh"
- "scripts/test-wallpaper-field-qml.sh"
- "scripts/test-card-menu-qml.sh"
- "scripts/test-orbit-daemon.py"
@@ -7723,3 +7723,11 @@ msgstr "System-Hintergrundbild"
#: crates/qbz-qt/qml/views/PlaylistView.qml
msgid "{} tracks will be removed from this playlist."
msgstr "{} Titel werden aus dieser Playlist entfernt."
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "Wallpaper blur"
msgstr "Unschärfe des Hintergrundbilds"
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "How much the wallpaper behind the window is blurred: 0% keeps it sharp, 100% is the strongest blur."
msgstr "Wie stark das Hintergrundbild hinter dem Fenster weichgezeichnet wird: 0 % lässt es scharf, 100 % ist die stärkste Unschärfe."
@@ -3378,3 +3378,11 @@ msgstr "System wallpaper"
#: crates/qbz-qt/qml/views/PlaylistView.qml
msgid "{} tracks will be removed from this playlist."
msgstr "{} tracks will be removed from this playlist."
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "Wallpaper blur"
msgstr "Wallpaper blur"
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "How much the wallpaper behind the window is blurred: 0% keeps it sharp, 100% is the strongest blur."
msgstr "How much the wallpaper behind the window is blurred: 0% keeps it sharp, 100% is the strongest blur."
@@ -7729,3 +7729,11 @@ msgstr "Fondo del sistema"
#: crates/qbz-qt/qml/views/PlaylistView.qml
msgid "{} tracks will be removed from this playlist."
msgstr "Se quitarán {} pistas de esta playlist."
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "Wallpaper blur"
msgstr "Desenfoque del fondo de pantalla"
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "How much the wallpaper behind the window is blurred: 0% keeps it sharp, 100% is the strongest blur."
msgstr "Qué tan desenfocado se ve el fondo de pantalla detrás de la ventana: 0 % lo deja nítido y 100 % es el desenfoque máximo."
@@ -7721,3 +7721,11 @@ msgstr "Fond d'écran du système"
#: crates/qbz-qt/qml/views/PlaylistView.qml
msgid "{} tracks will be removed from this playlist."
msgstr "{} titres seront retirés de cette playlist."
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "Wallpaper blur"
msgstr "Flou du fond d'écran"
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "How much the wallpaper behind the window is blurred: 0% keeps it sharp, 100% is the strongest blur."
msgstr "Intensité du flou du fond d'écran derrière la fenêtre : 0 % le laisse net, 100 % est le flou maximal."
@@ -7704,3 +7704,11 @@ msgstr "システムの壁紙"
#: crates/qbz-qt/qml/views/PlaylistView.qml
msgid "{} tracks will be removed from this playlist."
msgstr "{} 曲をこのプレイリストから削除します。"
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "Wallpaper blur"
msgstr "壁紙のぼかし"
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "How much the wallpaper behind the window is blurred: 0% keeps it sharp, 100% is the strongest blur."
msgstr "ウィンドウの背後に見える壁紙のぼかしの強さ。0% でくっきり、100% で最も強くぼかします。"
@@ -7711,3 +7711,11 @@ msgstr "Systeemachtergrond"
#: crates/qbz-qt/qml/views/PlaylistView.qml
msgid "{} tracks will be removed from this playlist."
msgstr "{} nummers worden uit deze afspeellijst verwijderd."
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "Wallpaper blur"
msgstr "Vervaging van de bureaubladachtergrond"
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "How much the wallpaper behind the window is blurred: 0% keeps it sharp, 100% is the strongest blur."
msgstr "Hoe sterk de bureaubladachtergrond achter het venster wordt vervaagd: 0% houdt hem scherp, 100% is de sterkste vervaging."
@@ -7721,3 +7721,11 @@ msgstr "Papel de parede do sistema"
#: crates/qbz-qt/qml/views/PlaylistView.qml
msgid "{} tracks will be removed from this playlist."
msgstr "{} faixas serão removidas desta playlist."
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "Wallpaper blur"
msgstr "Desfoque do papel de parede"
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "How much the wallpaper behind the window is blurred: 0% keeps it sharp, 100% is the strongest blur."
msgstr "Quanto o papel de parede atrás da janela fica desfocado: 0% o mantém nítido, 100% é o desfoque máximo."
@@ -7742,3 +7742,11 @@ msgstr "Обои системы"
#: crates/qbz-qt/qml/views/PlaylistView.qml
msgid "{} tracks will be removed from this playlist."
msgstr "Из этого плейлиста будет удалено {} треков."
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "Wallpaper blur"
msgstr "Размытие обоев"
#: crates/qbz-qt/qml/settings/AppearanceSettings.qml
msgid "How much the wallpaper behind the window is blurred: 0% keeps it sharp, 100% is the strongest blur."
msgstr "Насколько размыты обои за окном: 0 % — чётко, 100 % — максимальное размытие."
+1
View File
@@ -628,6 +628,7 @@ fn main() {
"qml/controls/IconTextButton.qml",
"qml/controls/SettingsButton.qml",
"qml/controls/SettingsDivider.qml",
"qml/controls/SettingsFieldset.qml",
"qml/controls/SettingsSpacer.qml",
// Moved out of views/local/ on 2026-07-31: the album/track
// CARD badges mount it too, and Slint keeps its counterpart in
@@ -0,0 +1,101 @@
// SettingsFieldset — a bordered, collapsible group of settings rows that
// belong to ONE control above them (the options of the selected theme).
//
// The border and the faint fill are what say "these rows are part of the
// same thing"; the header's chevron (> collapsed, v expanded) hides the body.
// The component does not own the collapsed state: the host passes the
// persisted value in and writes it back from `toggleRequested`, the same way
// the sidebar sections and the media-server panels keep theirs across
// restarts (settingsBool -> ui_prefs -> settingsJson).
//
// Rows go straight in as children; the body is a Column with a side inset,
// so a SettingRow still takes the full inner width.
import QtQuick
import com.blitzfc.qbz
import "../theme"
Rectangle {
id: root
property bool kioskHost: false
property string title: ""
property bool collapsed: false
signal toggleRequested()
default property alias content: body.data
QbzTheme { id: theme }
readonly property int inset: kioskHost ? 16 : 12
width: parent ? parent.width : 0
height: header.height + (collapsed ? 0 : body.height + inset)
radius: theme.radiusMd
color: theme.alphaTier(3)
border.width: 1
border.color: theme.borderMuted
Rectangle {
id: header
x: 1
y: 1
width: parent.width - 2
height: root.kioskHost ? 52 : 40
radius: theme.radiusMd
color: headerArea.containsMouse ? theme.elevatedHoverFill : "transparent"
activeFocusOnTab: true
border.width: activeFocus ? 2 : 0
border.color: theme.accent
Accessible.role: Accessible.Button
Accessible.name: root.title
Accessible.onPressAction: root.toggleRequested()
Keys.onPressed: function (event) {
if (!event.isAutoRepeat
&& (event.key === Qt.Key_Space
|| event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter)) {
root.toggleRequested()
event.accepted = true
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: root.inset - 2
anchors.verticalCenter: parent.verticalCenter
spacing: 8
QbzIcon {
anchors.verticalCenter: parent.verticalCenter
name: root.collapsed ? "chevron-right" : "chevron-down"
width: root.kioskHost ? 20 : 16
height: root.kioskHost ? 20 : 16
tintName: "secondary"
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.title
color: theme.textPrimary
font.pixelSize: root.kioskHost ? theme.fontBody * 1.2 : theme.fontBody
font.weight: theme.weightSemibold
}
}
MouseArea {
id: headerArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: header.forceActiveFocus()
onClicked: root.toggleRequested()
}
}
Column {
id: body
visible: !root.collapsed
x: root.inset
y: header.y + header.height
width: root.width - 2 * root.inset
spacing: 4
}
}
+170 -102
View File
@@ -98,6 +98,15 @@ Column {
readonly property bool tbLocked: doc.hideTitleBar === true || doc.useSystemTitleBar === true
// ============================ THEME ==================================
// ── EACH CHOICE'S OPTIONS SIT UNDER ITS OWN PICKER (2026-09-14) ────────
// The rows that exist only for one choice used to trail the whole group:
// Custom's editor and Auto's source rows came after Album header
// gradient, Compact header and Dynamic background, and Background image
// (an option of the Wallpaper background) ran straight into the Custom
// editor, so it read as part of the theme. Now each picker is followed by
// ITS options, boxed in a SettingsFieldset whose chevron state persists:
// the theme's right under Theme, the background's right under Dynamic
// background. The rows themselves are unchanged.
GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("THEME", QbzSession.trRev) }
SettingRow { kioskHost: root.kioskHost;
label: QbzSession.tr("Theme", QbzSession.trRev)
@@ -122,6 +131,113 @@ Column {
}
}
}
/// The applied theme's display name, the legend of its options box.
readonly property string currentThemeLabel: {
for (let i = 0; i < themeEntries.length; i++) {
if (themeEntries[i].slug === QbzShell.themeSlug) return themeEntries[i].label
}
return ""
}
// Auto-theme rows (the "auto" theme only).
SettingsFieldset { kioskHost: root.kioskHost;
visible: QbzShell.themeSlug === "auto"
title: root.currentThemeLabel
collapsed: root.doc.themeAutoCollapsed === true
onToggleRequested: QbzBridge.settingsBool("theme-auto-collapsed", root.doc.themeAutoCollapsed !== true)
SettingRow { kioskHost: root.kioskHost;
label: QbzSession.tr("Source", QbzSession.trRev)
description: QbzSession.tr("Generate a color theme from your system wallpaper or a custom image", QbzSession.trRev)
QbzSelect { kioskHost: root.kioskHost;
menuWidth: 200
options: root.doc.autoThemeSources || []
currentIndex: root.doc.autoThemeSourceIndex || 0
onSelected: function (i) { QbzBridge.settingsSelect("auto-theme-source", i) }
}
}
// "Custom Image" only (source index 2) — AppearanceSettings.slint:283-292.
// The row's LABEL is the picked path, which is how the reference shows
// what is currently in use; with nothing picked it repeats the button's
// text. Both strings already exist in the catalogs.
SettingRow { kioskHost: root.kioskHost;
visible: (root.doc.autoThemeSourceIndex || 0) === 2
label: (root.doc.autoThemeImagePath || "") !== ""
? root.doc.autoThemeImagePath
: QbzSession.tr("Select Image...", QbzSession.trRev)
SettingsButton { kioskHost: root.kioskHost;
text: QbzSession.tr("Select Image...", QbzSession.trRev)
// The action seam for button rows is settingsString with an empty
// payload — the `library-pick-folder` precedent
// (LibraryFolderTable.qml:111).
onClicked: QbzBridge.settingsString("auto-theme-select-image", "")
}
}
// The detected desktop, with the experimental caveat under it
// (`:294-297`). Hint only — no control, and it hides when the shared
// detector cannot name a desktop.
SettingRow { kioskHost: root.kioskHost;
visible: (root.doc.autoThemeDetectedDe || "") !== ""
label: QbzSession.tr("Detected: ", QbzSession.trRev)
+ (root.doc.autoThemeDetectedDe || "")
description: QbzSession.tr("Experimental: theme may not match your system exactly.", QbzSession.trRev)
}
SettingRow { kioskHost: root.kioskHost;
label: QbzSession.tr("Regenerate", QbzSession.trRev)
SettingsButton { kioskHost: root.kioskHost;
text: QbzSession.tr("Regenerate", QbzSession.trRev)
onClicked: QbzShell.themeSet(QbzShell.themeSlug)
}
}
}
// Custom-theme editor rows (the appended "Custom" theme only), 1:1 with
// AppearanceSettings.slint:313-425. "Start from current theme" re-seeds
// the base from the applied palette; the "Dark theme" toggle flips
// polarity; the grid below edits each base token and the rest of the
// palette is derived live in Rust on every change.
//
// There is deliberately NO save / save-as, NO delete, NO named-theme list
// and NO import/export: the model is ONE implicit custom theme that
// autosaves, and the reference has none of those affordances either.
SettingsFieldset { kioskHost: root.kioskHost;
visible: QbzShell.themeSlug === "custom"
title: root.currentThemeLabel
collapsed: root.doc.themeCustomCollapsed === true
onToggleRequested: {
// A picker left open inside a folded box would come back open.
customThemeEditor.closePicker()
QbzBridge.settingsBool("theme-custom-collapsed", root.doc.themeCustomCollapsed !== true)
}
SettingRow { kioskHost: root.kioskHost;
label: QbzSession.tr("Start from current theme", QbzSession.trRev)
description: QbzSession.tr("Copy the colors of the currently applied theme into the editor as a starting point.", QbzSession.trRev)
SettingsButton { kioskHost: root.kioskHost;
text: QbzSession.tr("Use current colors", QbzSession.trRev)
onClicked: {
QbzShell.customSeedFromCurrent()
customThemeEditor.closePicker()
}
}
}
SettingRow { kioskHost: root.kioskHost;
label: QbzSession.tr("Dark theme", QbzSession.trRev)
description: QbzSession.tr("Set the overall light or dark polarity. Affects derived shades, borders and overlays.", QbzSession.trRev)
QbzToggle { kioskHost: root.kioskHost;
checked: customThemeEditor.isDark
onToggled: function (v) { QbzShell.customToggleDark(v) }
}
}
// NOT inside a SettingRow: that control hardcodes 52/64px and centres one
// child, which would clip the ~250px picker. The reference keeps the grid
// outside its own rows for the same reason.
CustomThemeEditor { kioskHost: root.kioskHost;
id: customThemeEditor
visible: QbzShell.themeSlug === "custom"
}
}
SettingRow { kioskHost: root.kioskHost;
label: QbzSession.tr("Album header gradient", QbzSession.trRev)
description: QbzSession.tr("Use artwork-derived blur as a backdrop in album and artist detail views.", QbzSession.trRev)
@@ -148,113 +264,65 @@ Column {
onSelected: function (i) { QbzBridge.settingsSelect("app-background", i) }
}
}
// The picture behind the two Wallpaper modes: the desktop's, or one of
// the user's own (wallpaper_qt.rs).
SettingRow { kioskHost: root.kioskHost;
visible: (root.doc.appBackgroundIndex || 0) >= 3
label: QbzSession.tr("Background image", QbzSession.trRev)
description: (root.doc.appBackgroundImage || "") !== ""
? root.doc.appBackgroundImage
: QbzSession.tr("The system wallpaper. Choose an image of your own instead.", QbzSession.trRev)
Row {
spacing: 8
SettingsButton { kioskHost: root.kioskHost;
text: QbzSession.tr("Choose image…", QbzSession.trRev)
onClicked: QbzBridge.settingsPickBackgroundImage()
}
SettingsButton { kioskHost: root.kioskHost;
visible: (root.doc.appBackgroundImage || "") !== ""
text: QbzSession.tr("System wallpaper", QbzSession.trRev)
onClicked: QbzBridge.settingsClearBackgroundImage()
}
}
}
// Auto-theme rows (the "auto" theme only).
SettingRow { kioskHost: root.kioskHost;
visible: QbzShell.themeSlug === "auto"
label: QbzSession.tr("Source", QbzSession.trRev)
description: QbzSession.tr("Generate a color theme from your system wallpaper or a custom image", QbzSession.trRev)
QbzSelect { kioskHost: root.kioskHost;
menuWidth: 200
options: root.doc.autoThemeSources || []
currentIndex: root.doc.autoThemeSourceIndex || 0
onSelected: function (i) { QbzBridge.settingsSelect("auto-theme-source", i) }
}
}
// "Custom Image" only (source index 2) — AppearanceSettings.slint:283-292.
// The row's LABEL is the picked path, which is how the reference shows
// what is currently in use; with nothing picked it repeats the button's
// text. Both strings already exist in the catalogs.
SettingRow { kioskHost: root.kioskHost;
visible: QbzShell.themeSlug === "auto"
&& (root.doc.autoThemeSourceIndex || 0) === 2
label: (root.doc.autoThemeImagePath || "") !== ""
? root.doc.autoThemeImagePath
: QbzSession.tr("Select Image...", QbzSession.trRev)
SettingsButton { kioskHost: root.kioskHost;
text: QbzSession.tr("Select Image...", QbzSession.trRev)
// The action seam for button rows is settingsString with an empty
// payload — the `library-pick-folder` precedent
// (LibraryFolderTable.qml:111).
onClicked: QbzBridge.settingsString("auto-theme-select-image", "")
}
}
// The detected desktop, with the experimental caveat under it
// (`:294-297`). Hint only — no control, and it hides when the shared
// detector cannot name a desktop.
SettingRow { kioskHost: root.kioskHost;
visible: QbzShell.themeSlug === "auto"
&& (root.doc.autoThemeDetectedDe || "") !== ""
label: QbzSession.tr("Detected: ", QbzSession.trRev)
+ (root.doc.autoThemeDetectedDe || "")
description: QbzSession.tr("Experimental: theme may not match your system exactly.", QbzSession.trRev)
}
SettingRow { kioskHost: root.kioskHost;
visible: QbzShell.themeSlug === "auto"
label: QbzSession.tr("Regenerate", QbzSession.trRev)
SettingsButton { kioskHost: root.kioskHost;
text: QbzSession.tr("Regenerate", QbzSession.trRev)
onClicked: QbzShell.themeSet(QbzShell.themeSlug)
}
}
// The options of the chosen background: only the two Wallpaper modes
// (3 Wallpaper, 4 Wallpaper, blurred) have any.
readonly property int backgroundIndex: root.doc.appBackgroundIndex || 0
SettingsFieldset { kioskHost: root.kioskHost;
visible: root.backgroundIndex >= 3
title: (root.doc.appBackgroundModes || [])[root.backgroundIndex] || ""
collapsed: root.doc.ambientOptionsCollapsed === true
onToggleRequested: QbzBridge.settingsBool("ambient-options-collapsed", root.doc.ambientOptionsCollapsed !== true)
// Custom-theme editor rows (the appended "Custom" theme only), 1:1 with
// AppearanceSettings.slint:313-425. "Start from current theme" re-seeds
// the base from the applied palette; the "Dark theme" toggle flips
// polarity; the grid below edits each base token and the rest of the
// palette is derived live in Rust on every change.
//
// There is deliberately NO save / save-as, NO delete, NO named-theme list
// and NO import/export: the model is ONE implicit custom theme that
// autosaves, and the reference has none of those affordances either.
SettingRow { kioskHost: root.kioskHost;
visible: QbzShell.themeSlug === "custom"
label: QbzSession.tr("Start from current theme", QbzSession.trRev)
description: QbzSession.tr("Copy the colors of the currently applied theme into the editor as a starting point.", QbzSession.trRev)
SettingsButton { kioskHost: root.kioskHost;
text: QbzSession.tr("Use current colors", QbzSession.trRev)
onClicked: {
QbzShell.customSeedFromCurrent()
customThemeEditor.closePicker()
// The picture behind the two Wallpaper modes: the desktop's, or one of
// the user's own (wallpaper_qt.rs).
SettingRow { kioskHost: root.kioskHost;
label: QbzSession.tr("Background image", QbzSession.trRev)
description: (root.doc.appBackgroundImage || "") !== ""
? root.doc.appBackgroundImage
: QbzSession.tr("The system wallpaper. Choose an image of your own instead.", QbzSession.trRev)
Row {
spacing: 8
SettingsButton { kioskHost: root.kioskHost;
text: QbzSession.tr("Choose image…", QbzSession.trRev)
onClicked: QbzBridge.settingsPickBackgroundImage()
}
SettingsButton { kioskHost: root.kioskHost;
visible: (root.doc.appBackgroundImage || "") !== ""
text: QbzSession.tr("System wallpaper", QbzSession.trRev)
onClicked: QbzBridge.settingsClearBackgroundImage()
}
}
}
}
SettingRow { kioskHost: root.kioskHost;
visible: QbzShell.themeSlug === "custom"
label: QbzSession.tr("Dark theme", QbzSession.trRev)
description: QbzSession.tr("Set the overall light or dark polarity. Affects derived shades, borders and overlays.", QbzSession.trRev)
QbzToggle { kioskHost: root.kioskHost;
checked: customThemeEditor.isDark
onToggled: function (v) { QbzShell.customToggleDark(v) }
// Wallpaper mode only: how blurred the X-ray view of the desktop is.
// Qt's own range for MultiEffect.blur, 0.0-1.0, shown as 0-100 %; the
// default is the 0.75 the mode shipped with (settings_qt.rs). The drag
// moves the live background; the value is stored on release.
SettingRow { kioskHost: root.kioskHost;
visible: root.backgroundIndex === 3
label: QbzSession.tr("Wallpaper blur", QbzSession.trRev)
description: QbzSession.tr("How much the wallpaper behind the window is blurred: 0% keeps it sharp, 100% is the strongest blur.", QbzSession.trRev)
Row {
spacing: 12
QbzSlider { kioskHost: root.kioskHost;
id: wallpaperBlurSlider
anchors.verticalCenter: parent.verticalCenter
minimum: 0
maximum: 100
value: Math.round(QbzShell.wallpaperBlur * 100)
onChanged: function (v) { QbzShell.wallpaperBlur = v / 100 }
onReleased: function (v) { QbzBridge.settingsSlider("wallpaper-blur", v) }
}
Text {
anchors.verticalCenter: parent.verticalCenter
width: 40
horizontalAlignment: Text.AlignRight
text: wallpaperBlurSlider.value + "%"
color: theme.textSecondary
font.pixelSize: root.kioskHost ? theme.fontBody * 1.2 : theme.fontBody
}
}
}
}
// NOT inside a SettingRow: that control hardcodes 52/64px and centres one
// child, which would clip the ~250px picker. The reference keeps the grid
// outside its own rows for the same reason.
CustomThemeEditor { kioskHost: root.kioskHost;
id: customThemeEditor
visible: QbzShell.themeSlug === "custom"
}
SettingsSpacer { }
SettingsDivider { }
+3 -1
View File
@@ -140,7 +140,9 @@ Rectangle {
visible: root.wallpaperModeOn
source: root.wallpaperModeOn ? QbzShell.wallpaperUrl : ""
hostWindow: root.hostWindow
blur: 0.75
// The user's choice (Settings > Appearance, 0-100 % of Qt's 0.0-1.0),
// live while that slider drags; 0.75 by default.
blur: QbzShell.wallpaperBlur
dim: theme.isDark ? QbzShell.ambientDim : 0.0
}
Rectangle {
+98
View File
@@ -665,6 +665,30 @@ pub fn app_background_mode() -> i32 {
.unwrap_or(0) as i32
}
/// The Wallpaper mode's blur, in MultiEffect's own units: Qt documents
/// `blur` from 0.0 (no blur) to 1.0 (full blur), reached at the field's
/// `blurMax` of 64 (shell/WallpaperField.qml). 0.75 is the look the mode
/// shipped with and stays the default; Settings > Appearance edits it as
/// 0-100 %.
pub const WALLPAPER_BLUR_DEFAULT: f32 = 0.75;
pub fn wallpaper_blur() -> f32 {
clamp_wallpaper_blur(pref_f32("wallpaper_blur", WALLPAPER_BLUR_DEFAULT))
}
/// Qt's range, whatever a hand-edited ui_prefs.json holds; NaN is the default.
fn clamp_wallpaper_blur(value: f32) -> f32 {
if value.is_nan() {
return WALLPAPER_BLUR_DEFAULT;
}
value.clamp(0.0, 1.0)
}
/// The Settings slider's 0-100 % step as the stored Qt value.
fn wallpaper_blur_from_percent(percent: i32) -> f32 {
percent.clamp(0, 100) as f32 / 100.0
}
/// Live-tuning knobs (Slint AppearanceState defaults; the QBZ_BG_* envs are
/// the same dev knobs the Slint seeds at startup).
pub fn ambient_dim() -> f32 {
@@ -2188,6 +2212,15 @@ pub struct SettingsDoc {
/// The user's own background image ("" = the desktop wallpaper).
#[serde(rename = "appBackgroundImage")]
pub app_background_image: String,
/// Persisted fieldset state of the Appearance > Theme groups (default
/// expanded): the Custom theme editor, the Auto theme rows, and the
/// options of the selected dynamic background.
#[serde(rename = "themeCustomCollapsed")]
pub theme_custom_collapsed: bool,
#[serde(rename = "themeAutoCollapsed")]
pub theme_auto_collapsed: bool,
#[serde(rename = "ambientOptionsCollapsed")]
pub ambient_options_collapsed: bool,
#[serde(rename = "autoThemeSources")]
pub auto_theme_sources: Vec<String>,
#[serde(rename = "autoThemeSourceIndex")]
@@ -2748,6 +2781,9 @@ pub async fn publish_snapshot() {
.position(|v| *v == pref_str("app_background", "off"))
.unwrap_or(0) as i32,
app_background_image: pref_str("app_background_image", ""),
theme_custom_collapsed: pref_bool("theme_custom_collapsed", false),
theme_auto_collapsed: pref_bool("theme_auto_collapsed", false),
ambient_options_collapsed: pref_bool("ambient_options_collapsed", false),
auto_theme_sources: AUTO_THEME_SOURCE_LABELS
.iter()
.map(|l| qbz_i18n::t(l))
@@ -3689,6 +3725,19 @@ pub async fn settings_bool(runtime: &Arc<AppRuntime<LoggingAdapter>>, key: &str,
save_pref("myqbz_collections_collapsed", serde_json::json!(value));
Ok(Apply::None)
}
// Appearance > Theme fieldsets, read back from settingsJson.
"theme-custom-collapsed" => {
save_pref("theme_custom_collapsed", serde_json::json!(value));
Ok(Apply::None)
}
"theme-auto-collapsed" => {
save_pref("theme_auto_collapsed", serde_json::json!(value));
Ok(Apply::None)
}
"ambient-options-collapsed" => {
save_pref("ambient_options_collapsed", serde_json::json!(value));
Ok(Apply::None)
}
"sidebar-playlist-collage" => {
save_pref("sidebar_playlist_collage", serde_json::json!(value));
// LIVE: the sidebar rows swap collage <-> list-music glyph off the
@@ -4409,6 +4458,15 @@ pub async fn settings_select(runtime: &Arc<AppRuntime<LoggingAdapter>>, key: &st
}
pub async fn settings_slider(runtime: &Arc<AppRuntime<LoggingAdapter>>, key: &str, value: i32) {
if key == "wallpaper-blur" {
// Persisted on the slider's release; the drag itself already moved
// QbzShell.wallpaperBlur for the live preview.
let blur = wallpaper_blur_from_percent(value);
// Written from the whole percent as f64: an f32 widened on the way to
// JSON would store 0.62 as 0.6200000047683716.
save_pref("wallpaper_blur", serde_json::json!(f64::from(value.clamp(0, 100)) / 100.0));
crate::shell_bridge::ui(move |mut b| b.as_mut().set_wallpaper_blur(blur));
}
if key == "buffer-seconds" {
let seconds = value.clamp(1, 10) as u8;
match with_audio(|s| s.set_stream_buffer_seconds(seconds)) {
@@ -4882,6 +4940,46 @@ mod local_tab_order_tests {
}
}
#[cfg(test)]
mod wallpaper_blur_tests {
use super::*;
#[test]
fn the_slider_percent_maps_onto_qts_blur_range() {
assert_eq!(wallpaper_blur_from_percent(0), 0.0);
assert_eq!(wallpaper_blur_from_percent(75), 0.75);
assert_eq!(wallpaper_blur_from_percent(100), 1.0);
assert_eq!(wallpaper_blur_from_percent(-20), 0.0);
assert_eq!(wallpaper_blur_from_percent(250), 1.0);
}
#[test]
fn a_hand_edited_value_is_held_to_qts_range() {
assert_eq!(clamp_wallpaper_blur(1.7), 1.0);
assert_eq!(clamp_wallpaper_blur(-0.2), 0.0);
assert_eq!(clamp_wallpaper_blur(0.4), 0.4);
assert_eq!(clamp_wallpaper_blur(f32::NAN), WALLPAPER_BLUR_DEFAULT);
}
}
#[cfg(test)]
mod theme_fieldset_doc_tests {
use super::*;
#[test]
fn the_settings_document_names_the_fieldset_states_for_qml() {
let doc = SettingsDoc {
theme_custom_collapsed: true,
ambient_options_collapsed: true,
..Default::default()
};
let json = serde_json::to_value(&doc).expect("settings document serializes");
assert_eq!(json["themeCustomCollapsed"], true);
assert_eq!(json["themeAutoCollapsed"], false);
assert_eq!(json["ambientOptionsCollapsed"], true);
}
}
#[cfg(test)]
mod exclusive_gate_tests {
use super::*;
+6
View File
@@ -346,6 +346,10 @@ pub mod qbz_shell {
#[qproperty(QString, ambient_accent)]
// Look knobs (Slint AppearanceState defaults; QBZ_BG_* env seed).
#[qproperty(f32, ambient_dim)]
/// The Wallpaper mode's blur, MultiEffect units 0.0-1.0
/// (settings_qt::wallpaper_blur). Settings writes it while the slider
/// drags, for a live preview, and persists it on release.
#[qproperty(f32, wallpaper_blur)]
/// Debug knob QBZ_BG_SCALE — RETIRED 2026-08-13: the ambient field
/// renders inline now (no offscreen target to scale), and the knob
/// measured as a non-lever anyway. Kept to avoid bridge churn.
@@ -923,6 +927,7 @@ pub struct QbzShellRust {
ambient_secondary: QString,
ambient_accent: QString,
ambient_dim: f32,
wallpaper_blur: f32,
ambient_scale: f32,
viz_tick_ms: i32,
pane_layer: bool,
@@ -1043,6 +1048,7 @@ impl Default for QbzShellRust {
ambient_secondary: QString::from("#9632ff"),
ambient_accent: QString::from("#3fd9c8"),
ambient_dim: crate::settings_qt::ambient_dim(),
wallpaper_blur: crate::settings_qt::wallpaper_blur(),
ambient_scale: crate::settings_qt::ambient_scale(),
viz_tick_ms: crate::settings_qt::viz_tick_ms(),
pane_layer: crate::settings_qt::pane_layer(),
@@ -0,0 +1,81 @@
// SettingsFieldset: the bordered, collapsible group that holds the options of
// ONE settings control (Appearance > Theme: the theme's editor, the Wallpaper
// background's image and blur). The host owns the persisted collapsed state;
// the fieldset shows or hides its rows and asks for the toggle.
import QtQuick
import QtTest
import com.blitzfc.qbz
import "../../crates/qbz-qt/qml/controls"
Item {
id: root
width: 700; height: 600
property bool storedCollapsed: false
property int toggles: 0
Column {
width: 600
SettingsFieldset {
id: fieldset
title: "Custom"
collapsed: root.storedCollapsed
onToggleRequested: {
root.toggles++
root.storedCollapsed = !root.storedCollapsed
}
Rectangle { id: rowA; width: parent.width; height: 52; color: "transparent" }
Rectangle { id: rowB; width: parent.width; height: 64; color: "transparent" }
}
Rectangle { id: below; width: 600; height: 20; color: "transparent" }
}
TestCase {
name: "SettingsFieldset"
when: windowShown
function init() {
root.storedCollapsed = false
root.toggles = 0
wait(0)
}
function test_expanded_shows_its_rows_inside_the_box() {
verify(rowA.visible && rowB.visible)
compare(rowA.width, fieldset.width - 2 * fieldset.inset, "rows take the inner width")
var bottom = rowB.mapToItem(fieldset, 0, rowB.height).y
verify(bottom <= fieldset.height, "the last row sits inside the border")
compare(below.y, fieldset.height, "the next setting starts after the box")
}
function test_header_click_asks_for_the_toggle_and_folds_the_body() {
var expandedHeight = fieldset.height
mouseClick(fieldset, 60, 20)
compare(root.toggles, 1)
verify(fieldset.collapsed)
verify(!rowA.visible && !rowB.visible, "a folded box hides its rows")
verify(fieldset.height < expandedHeight)
// Column lays out in the next polish pass.
tryCompare(below, "y", fieldset.height, 1000, "the settings below move up")
mouseClick(fieldset, 60, 20)
compare(root.toggles, 2)
verify(rowA.visible)
compare(fieldset.height, expandedHeight)
}
function test_keyboard_toggles_from_the_focused_header() {
mouseClick(fieldset, 60, 20)
compare(root.toggles, 1)
keyClick(Qt.Key_Space)
compare(root.toggles, 2)
keyClick(Qt.Key_Return)
compare(root.toggles, 3)
}
function test_a_click_on_a_row_does_not_toggle() {
var p = rowA.mapToItem(fieldset, 20, 20)
mouseClick(fieldset, p.x, p.y)
compare(root.toggles, 0)
}
}
}
+1
View File
@@ -16,6 +16,7 @@ bash scripts/test-library-folders-qml.sh
bash scripts/test-array-model-qml.sh
bash scripts/test-keyed-model-qml.sh
bash scripts/test-multi-select-qml.sh
bash scripts/test-settings-fieldset-qml.sh
bash scripts/test-wallpaper-field-qml.sh
bash scripts/test-card-menu-qml.sh
bash scripts/test-orbit-qml.sh
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# SettingsFieldset regression: the collapsible options box of Appearance >
# Theme shows its rows inside the border, folds them on a header click or key,
# moves the settings below, and leaves the persisted state to its host. Same
# run in CI and locally.
set -euo pipefail
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
if [[ -n "${QT_ROOT_DIR:-}" ]]; then
qt_fieldset_bins="$QT_ROOT_DIR/bin"
else
qt_fieldset_qmake="${QMAKE:-}"
if [[ -z "$qt_fieldset_qmake" ]]; then
qt_fieldset_qmake="$(command -v qmake6 || command -v qmake)"
fi
qt_fieldset_bins="$("$qt_fieldset_qmake" -query QT_INSTALL_BINS)"
fi
QT_QPA_PLATFORM=offscreen QT_QUICK_BACKEND=software \
"$qt_fieldset_bins/qmltestrunner" \
-input scripts/qml-tests/tst_settings_fieldset.qml \
-import scripts/qml-tests/imports