nosignal-shell/modules/controlcenter/appearance/AppearancePane.qml

1527 lines
72 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

pragma ComponentBehavior: Bound
import ".."
import "../components"
import "../../launcher/services"
import qs.components
import qs.components.controls
import qs.components.effects
import qs.components.containers
import qs.components.images
import qs.services
import qs.config
import qs.utils
import Caelestia.Models
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property Session session
property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1
property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded"
property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF"
property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik"
property real fontSizeScale: Config.appearance.font.size.scale ?? 1
property real paddingScale: Config.appearance.padding.scale ?? 1
property real roundingScale: Config.appearance.rounding.scale ?? 1
property real spacingScale: Config.appearance.spacing.scale ?? 1
property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false
property real transparencyBase: Config.appearance.transparency.base ?? 0.85
property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4
property real borderRounding: Config.border.rounding ?? 1
property real borderThickness: Config.border.thickness ?? 1
property bool desktopClockEnabled: Config.background.desktopClock.enabled ?? false
property bool backgroundEnabled: Config.background.enabled ?? true
property bool visualiserEnabled: Config.background.visualiser.enabled ?? false
property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true
property real visualiserRounding: Config.background.visualiser.rounding ?? 1
property real visualiserSpacing: Config.background.visualiser.spacing ?? 1
anchors.fill: parent
function saveConfig() {
Config.appearance.anim.durations.scale = root.animDurationsScale;
Config.appearance.font.family.material = root.fontFamilyMaterial;
Config.appearance.font.family.mono = root.fontFamilyMono;
Config.appearance.font.family.sans = root.fontFamilySans;
Config.appearance.font.size.scale = root.fontSizeScale;
Config.appearance.padding.scale = root.paddingScale;
Config.appearance.rounding.scale = root.roundingScale;
Config.appearance.spacing.scale = root.spacingScale;
Config.appearance.transparency.enabled = root.transparencyEnabled;
Config.appearance.transparency.base = root.transparencyBase;
Config.appearance.transparency.layers = root.transparencyLayers;
Config.background.desktopClock.enabled = root.desktopClockEnabled;
Config.background.enabled = root.backgroundEnabled;
Config.background.visualiser.enabled = root.visualiserEnabled;
Config.background.visualiser.autoHide = root.visualiserAutoHide;
Config.background.visualiser.rounding = root.visualiserRounding;
Config.background.visualiser.spacing = root.visualiserSpacing;
Config.border.rounding = root.borderRounding;
Config.border.thickness = root.borderThickness;
Config.save();
}
Component {
id: appearanceRightContentComponent
StyledFlickable {
id: rightAppearanceFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: contentLayout.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: rightAppearanceFlickable
}
ColumnLayout {
id: contentLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: Appearance.spacing.normal
SettingsHeader {
icon: "palette"
title: qsTr("Appearance Settings")
}
StyledText {
Layout.topMargin: Appearance.spacing.large
Layout.alignment: Qt.AlignHCenter
text: qsTr("Wallpaper")
font.pointSize: Appearance.font.size.extraLarge
font.weight: 600
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Select a wallpaper")
font.pointSize: Appearance.font.size.normal
color: Colours.palette.m3onSurfaceVariant
}
Item {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.large
Layout.preferredHeight: wallpaperLoader.item ? wallpaperLoader.item.layoutPreferredHeight : 0
Loader {
id: wallpaperLoader
anchors.fill: parent
asynchronous: true
active: {
const isActive = root.session.activeIndex === 3;
const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1;
const splitLayout = root.children[0];
const loader = splitLayout && splitLayout.rightLoader ? splitLayout.rightLoader : null;
const shouldActivate = loader && loader.item !== null && (isActive || isAdjacent);
return shouldActivate;
}
onStatusChanged: {
if (status === Loader.Error) {
console.error("[AppearancePane] Wallpaper loader error!");
}
}
// Stop lazy loading when loader becomes inactive
onActiveChanged: {
if (!active && wallpaperLoader.item) {
const container = wallpaperLoader.item;
if (container && container.wallpaperGrid) {
const grid = container.wallpaperGrid;
if (grid.imageUpdateTimer) {
grid.imageUpdateTimer.stop();
}
}
}
}
sourceComponent: Item {
id: wallpaperGridContainer
property alias layoutPreferredHeight: wallpaperGrid.layoutPreferredHeight
// Find and store reference to parent Flickable for scroll monitoring
property var parentFlickable: {
let item = parent;
while (item) {
if (item.flickableDirection !== undefined) {
return item;
}
item = item.parent;
}
return null;
}
// Cleanup when component is destroyed
Component.onDestruction: {
if (wallpaperGrid) {
if (wallpaperGrid.scrollCheckTimer) {
wallpaperGrid.scrollCheckTimer.stop();
}
wallpaperGrid._expansionInProgress = false;
}
}
QtObject {
id: lazyModel
property var sourceList: null
property int loadedCount: 0
property int visibleCount: 0
property int totalCount: 0
function initialize(list) {
sourceList = list;
totalCount = list ? list.length : 0;
const initialRows = 3;
const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 3;
const initialCount = Math.min(initialRows * cols, totalCount);
loadedCount = initialCount;
visibleCount = initialCount;
}
function loadOneRow() {
if (loadedCount < totalCount) {
const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 1;
const itemsToLoad = Math.min(cols, totalCount - loadedCount);
loadedCount += itemsToLoad;
}
}
function updateVisibleCount(neededCount) {
const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 1;
const maxVisible = Math.min(neededCount, loadedCount);
const rows = Math.ceil(maxVisible / cols);
const newVisibleCount = Math.min(rows * cols, loadedCount);
if (newVisibleCount > visibleCount) {
visibleCount = newVisibleCount;
}
}
}
GridView {
id: wallpaperGrid
anchors.fill: parent
property int _delegateCount: 0
readonly property int minCellWidth: 200 + Appearance.spacing.normal
readonly property int columnsCount: Math.max(1, Math.floor(parent.width / minCellWidth))
readonly property int layoutPreferredHeight: {
if (!lazyModel || lazyModel.visibleCount === 0 || columnsCount === 0) {
return 0;
}
const calculated = Math.ceil(lazyModel.visibleCount / columnsCount) * cellHeight;
return calculated;
}
height: layoutPreferredHeight
cellWidth: width / columnsCount
cellHeight: 140 + Appearance.spacing.normal
leftMargin: 0
rightMargin: 0
topMargin: 0
bottomMargin: 0
ListModel {
id: wallpaperListModel
}
model: wallpaperListModel
Connections {
target: lazyModel
function onVisibleCountChanged(): void {
if (!lazyModel || !lazyModel.sourceList) return;
const newCount = lazyModel.visibleCount;
const currentCount = wallpaperListModel.count;
if (newCount > currentCount) {
const flickable = wallpaperGridContainer.parentFlickable;
const oldScrollY = flickable ? flickable.contentY : 0;
for (let i = currentCount; i < newCount; i++) {
wallpaperListModel.append({modelData: lazyModel.sourceList[i]});
}
if (flickable) {
Qt.callLater(function() {
if (Math.abs(flickable.contentY - oldScrollY) < 1) {
flickable.contentY = oldScrollY;
}
});
}
}
}
}
Component.onCompleted: {
Qt.callLater(function() {
const isActive = root.session.activeIndex === 3;
if (width > 0 && parent && parent.visible && isActive && Wallpapers.list) {
lazyModel.initialize(Wallpapers.list);
wallpaperListModel.clear();
for (let i = 0; i < lazyModel.visibleCount; i++) {
wallpaperListModel.append({modelData: lazyModel.sourceList[i]});
}
}
});
}
Connections {
target: root.session
function onActiveIndexChanged(): void {
const isActive = root.session.activeIndex === 3;
// Stop lazy loading when switching away from appearance pane
if (!isActive) {
if (scrollCheckTimer) {
scrollCheckTimer.stop();
}
if (wallpaperGrid) {
wallpaperGrid._expansionInProgress = false;
}
return;
}
// Initialize if needed when switching to appearance pane
if (isActive && width > 0 && !lazyModel.sourceList && parent && parent.visible && Wallpapers.list) {
lazyModel.initialize(Wallpapers.list);
wallpaperListModel.clear();
for (let i = 0; i < lazyModel.visibleCount; i++) {
wallpaperListModel.append({modelData: lazyModel.sourceList[i]});
}
}
}
}
onWidthChanged: {
const isActive = root.session.activeIndex === 3;
if (width > 0 && !lazyModel.sourceList && parent && parent.visible && isActive && Wallpapers.list) {
lazyModel.initialize(Wallpapers.list);
wallpaperListModel.clear();
for (let i = 0; i < lazyModel.visibleCount; i++) {
wallpaperListModel.append({modelData: lazyModel.sourceList[i]});
}
}
}
// Force true lazy loading: only create delegates for visible items
displayMarginBeginning: 0
displayMarginEnd: 0
cacheBuffer: 0
// Debounce expansion to avoid too frequent checks
property bool _expansionInProgress: false
Connections {
target: wallpaperGridContainer.parentFlickable
function onContentYChanged(): void {
// Don't process scroll events if appearance pane is not active
const isActive = root.session.activeIndex === 3;
if (!isActive) return;
if (!lazyModel || !lazyModel.sourceList || lazyModel.loadedCount >= lazyModel.totalCount || wallpaperGrid._expansionInProgress) {
return;
}
const flickable = wallpaperGridContainer.parentFlickable;
if (!flickable) return;
const gridY = wallpaperGridContainer.y;
const scrollY = flickable.contentY;
const viewportHeight = flickable.height;
const topY = scrollY - gridY;
const bottomY = scrollY + viewportHeight - gridY;
if (bottomY < 0) return;
const topRow = Math.max(0, Math.floor(topY / wallpaperGrid.cellHeight));
const bottomRow = Math.floor(bottomY / wallpaperGrid.cellHeight);
// Update visible count with 1 row buffer ahead
const bufferRows = 1;
const neededBottomRow = bottomRow + bufferRows;
const neededCount = Math.min((neededBottomRow + 1) * wallpaperGrid.columnsCount, lazyModel.loadedCount);
lazyModel.updateVisibleCount(neededCount);
const loadedRows = Math.ceil(lazyModel.loadedCount / wallpaperGrid.columnsCount);
const rowsRemaining = loadedRows - (bottomRow + 1);
if (rowsRemaining <= 1 && lazyModel.loadedCount < lazyModel.totalCount) {
if (!wallpaperGrid._expansionInProgress) {
wallpaperGrid._expansionInProgress = true;
lazyModel.loadOneRow();
Qt.callLater(function() {
wallpaperGrid._expansionInProgress = false;
});
}
}
}
}
// Fallback timer to check scroll position periodically
Timer {
id: scrollCheckTimer
interval: 100
running: {
const isActive = root.session.activeIndex === 3;
return isActive && lazyModel && lazyModel.sourceList && lazyModel.loadedCount < lazyModel.totalCount;
}
repeat: true
onTriggered: {
// Double-check that appearance pane is still active
const isActive = root.session.activeIndex === 3;
if (!isActive) {
stop();
return;
}
const flickable = wallpaperGridContainer.parentFlickable;
if (!flickable || !lazyModel || !lazyModel.sourceList) return;
const gridY = wallpaperGridContainer.y;
const scrollY = flickable.contentY;
const viewportHeight = flickable.height;
const topY = scrollY - gridY;
const bottomY = scrollY + viewportHeight - gridY;
if (bottomY < 0) return;
const topRow = Math.max(0, Math.floor(topY / wallpaperGrid.cellHeight));
const bottomRow = Math.floor(bottomY / wallpaperGrid.cellHeight);
const bufferRows = 1;
const neededBottomRow = bottomRow + bufferRows;
const neededCount = Math.min((neededBottomRow + 1) * wallpaperGrid.columnsCount, lazyModel.loadedCount);
lazyModel.updateVisibleCount(neededCount);
const loadedRows = Math.ceil(lazyModel.loadedCount / wallpaperGrid.columnsCount);
const rowsRemaining = loadedRows - (bottomRow + 1);
if (rowsRemaining <= 1 && lazyModel.loadedCount < lazyModel.totalCount) {
if (!wallpaperGrid._expansionInProgress) {
wallpaperGrid._expansionInProgress = true;
lazyModel.loadOneRow();
Qt.callLater(function() {
wallpaperGrid._expansionInProgress = false;
});
}
}
}
}
interactive: false
delegate: Item {
required property var modelData
width: wallpaperGrid.cellWidth
height: wallpaperGrid.cellHeight
readonly property bool isCurrent: modelData.path === Wallpapers.actualCurrent
readonly property real itemMargin: Appearance.spacing.normal / 2
readonly property real itemRadius: Appearance.rounding.normal
Component.onCompleted: {
wallpaperGrid._delegateCount++;
}
StateLayer {
anchors.fill: parent
anchors.leftMargin: itemMargin
anchors.rightMargin: itemMargin
anchors.topMargin: itemMargin
anchors.bottomMargin: itemMargin
radius: itemRadius
function onClicked(): void {
Wallpapers.setWallpaper(modelData.path);
}
}
StyledClippingRect {
id: image
anchors.fill: parent
anchors.leftMargin: itemMargin
anchors.rightMargin: itemMargin
anchors.topMargin: itemMargin
anchors.bottomMargin: itemMargin
color: Colours.tPalette.m3surfaceContainer
radius: itemRadius
antialiasing: true
layer.enabled: true
layer.smooth: true
CachingImage {
id: cachingImage
path: modelData.path
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
cache: true
visible: opacity > 0
antialiasing: true
smooth: true
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 1000
easing.type: Easing.OutQuad
}
}
}
// Fallback if CachingImage fails to load
Image {
id: fallbackImage
anchors.fill: parent
source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : ""
asynchronous: true
fillMode: Image.PreserveAspectCrop
cache: true
visible: opacity > 0
antialiasing: true
smooth: true
opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 1000
easing.type: Easing.OutQuad
}
}
}
Timer {
id: fallbackTimer
property bool triggered: false
interval: 800
running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null
onTriggered: triggered = true
}
// Gradient overlay for filename
Rectangle {
id: filenameOverlay
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5
radius: 0
gradient: Gradient {
GradientStop {
position: 0.0
color: Qt.rgba(Colours.palette.m3surfaceContainer.r,
Colours.palette.m3surfaceContainer.g,
Colours.palette.m3surfaceContainer.b, 0)
}
GradientStop {
position: 0.3
color: Qt.rgba(Colours.palette.m3surfaceContainer.r,
Colours.palette.m3surfaceContainer.g,
Colours.palette.m3surfaceContainer.b, 0.7)
}
GradientStop {
position: 0.6
color: Qt.rgba(Colours.palette.m3surfaceContainer.r,
Colours.palette.m3surfaceContainer.g,
Colours.palette.m3surfaceContainer.b, 0.9)
}
GradientStop {
position: 1.0
color: Qt.rgba(Colours.palette.m3surfaceContainer.r,
Colours.palette.m3surfaceContainer.g,
Colours.palette.m3surfaceContainer.b, 0.95)
}
}
opacity: 0
Behavior on opacity {
NumberAnimation {
duration: 1000
easing.type: Easing.OutCubic
}
}
Component.onCompleted: {
opacity = 1;
}
}
}
Rectangle {
anchors.fill: parent
anchors.leftMargin: itemMargin
anchors.rightMargin: itemMargin
anchors.topMargin: itemMargin
anchors.bottomMargin: itemMargin
color: "transparent"
radius: itemRadius + border.width
border.width: isCurrent ? 2 : 0
border.color: Colours.palette.m3primary
antialiasing: true
smooth: true
Behavior on border.width {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
MaterialIcon {
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Appearance.padding.small
visible: isCurrent
text: "check_circle"
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.large
}
}
StyledText {
id: filenameText
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
anchors.bottomMargin: Appearance.padding.normal
readonly property string fileName: {
const path = modelData.relativePath || "";
const parts = path.split("/");
return parts.length > 0 ? parts[parts.length - 1] : path;
}
text: fileName
font.pointSize: Appearance.font.size.smaller
font.weight: 500
color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface
elide: Text.ElideMiddle
maximumLineCount: 1
horizontalAlignment: Text.AlignHCenter
opacity: 0
Behavior on opacity {
NumberAnimation {
duration: 1000
easing.type: Easing.OutCubic
}
}
Component.onCompleted: {
opacity = 1;
}
}
}
}
}
}
}
}
}
}
SplitPaneLayout {
anchors.fill: parent
leftContent: Component {
StyledFlickable {
id: sidebarFlickable
readonly property var rootPane: root
flickableDirection: Flickable.VerticalFlick
contentHeight: sidebarLayout.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: sidebarFlickable
}
ColumnLayout {
id: sidebarLayout
anchors.left: parent.left
anchors.right: parent.right
spacing: Appearance.spacing.small
readonly property bool allSectionsExpanded:
themeModeSection.expanded &&
colorVariantSection.expanded &&
colorSchemeSection.expanded &&
animationsSection.expanded &&
fontsSection.expanded &&
scalesSection.expanded &&
transparencySection.expanded &&
borderSection.expanded &&
backgroundSection.expanded
RowLayout {
spacing: Appearance.spacing.smaller
StyledText {
text: qsTr("Appearance")
font.pointSize: Appearance.font.size.large
font.weight: 500
}
Item {
Layout.fillWidth: true
}
IconButton {
icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more"
type: IconButton.Text
label.animate: true
onClicked: {
const shouldExpand = !sidebarLayout.allSectionsExpanded;
themeModeSection.expanded = shouldExpand;
colorVariantSection.expanded = shouldExpand;
colorSchemeSection.expanded = shouldExpand;
animationsSection.expanded = shouldExpand;
fontsSection.expanded = shouldExpand;
scalesSection.expanded = shouldExpand;
transparencySection.expanded = shouldExpand;
borderSection.expanded = shouldExpand;
backgroundSection.expanded = shouldExpand;
}
}
}
CollapsibleSection {
id: themeModeSection
title: qsTr("Theme mode")
description: qsTr("Light or dark theme")
showBackground: true
SwitchRow {
label: qsTr("Dark mode")
checked: !Colours.currentLight
onToggled: checked => {
Colours.setMode(checked ? "dark" : "light");
}
}
}
CollapsibleSection {
id: colorVariantSection
title: qsTr("Color variant")
description: qsTr("Material theme variant")
showBackground: true
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small / 2
Repeater {
model: M3Variants.list
delegate: StyledRect {
required property var modelData
Layout.fillWidth: true
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
border.width: modelData.variant === Schemes.currentVariant ? 1 : 0
border.color: Colours.palette.m3primary
StateLayer {
function onClicked(): void {
const variant = modelData.variant;
Schemes.currentVariant = variant;
Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]);
Qt.callLater(() => {
reloadTimer.restart();
});
}
}
Timer {
id: reloadTimer
interval: 300
onTriggered: {
Schemes.reload();
}
}
RowLayout {
id: variantRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
MaterialIcon {
text: modelData.icon
font.pointSize: Appearance.font.size.large
fill: modelData.variant === Schemes.currentVariant ? 1 : 0
}
StyledText {
Layout.fillWidth: true
text: modelData.name
font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400
}
MaterialIcon {
visible: modelData.variant === Schemes.currentVariant
text: "check"
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.large
}
}
implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2
}
}
}
}
CollapsibleSection {
id: colorSchemeSection
title: qsTr("Color scheme")
description: qsTr("Available color schemes")
showBackground: true
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small / 2
Repeater {
model: Schemes.list
delegate: StyledRect {
required property var modelData
Layout.fillWidth: true
readonly property string schemeKey: `${modelData.name} ${modelData.flavour}`
readonly property bool isCurrent: schemeKey === Schemes.currentScheme
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
border.width: isCurrent ? 1 : 0
border.color: Colours.palette.m3primary
StateLayer {
function onClicked(): void {
const name = modelData.name;
const flavour = modelData.flavour;
const schemeKey = `${name} ${flavour}`;
Schemes.currentScheme = schemeKey;
Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]);
Qt.callLater(() => {
reloadTimer.restart();
});
}
}
Timer {
id: reloadTimer
interval: 300
onTriggered: {
Schemes.reload();
}
}
RowLayout {
id: schemeRow
anchors.fill: parent
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledRect {
id: preview
Layout.alignment: Qt.AlignVCenter
border.width: 1
border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5)
color: `#${modelData.colours?.surface}`
radius: Appearance.rounding.full
implicitWidth: iconPlaceholder.implicitWidth
implicitHeight: iconPlaceholder.implicitWidth
MaterialIcon {
id: iconPlaceholder
visible: false
text: "circle"
font.pointSize: Appearance.font.size.large
}
Item {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
implicitWidth: parent.implicitWidth / 2
clip: true
StyledRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
implicitWidth: preview.implicitWidth
color: `#${modelData.colours?.primary}`
radius: Appearance.rounding.full
}
}
}
Column {
Layout.fillWidth: true
spacing: 0
StyledText {
text: modelData.flavour ?? ""
font.pointSize: Appearance.font.size.normal
}
StyledText {
text: modelData.name ?? ""
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3outline
elide: Text.ElideRight
anchors.left: parent.left
anchors.right: parent.right
}
}
Loader {
active: isCurrent
asynchronous: true
sourceComponent: MaterialIcon {
text: "check"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
}
}
}
implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2
}
}
}
}
CollapsibleSection {
id: animationsSection
title: qsTr("Animations")
showBackground: true
SectionContainer {
contentSpacing: Appearance.spacing.normal
SliderInput {
Layout.fillWidth: true
label: qsTr("Animation duration scale")
value: rootPane.animDurationsScale
from: 0.1
to: 5.0
decimals: 1
suffix: "×"
validator: DoubleValidator { bottom: 0.1; top: 5.0 }
onValueModified: (newValue) => {
rootPane.animDurationsScale = newValue;
rootPane.saveConfig();
}
}
}
}
CollapsibleSection {
id: fontsSection
title: qsTr("Fonts")
showBackground: true
CollapsibleSection {
id: materialFontSection
title: qsTr("Material font family")
expanded: true
showBackground: true
nested: true
Loader {
id: materialFontLoader
Layout.fillWidth: true
Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0
asynchronous: true
active: materialFontSection.expanded
sourceComponent: StyledListView {
id: materialFontList
property alias contentHeight: materialFontList.contentHeight
clip: true
spacing: Appearance.spacing.small / 2
model: Qt.fontFamilies()
StyledScrollBar.vertical: StyledScrollBar {
flickable: materialFontList
}
delegate: StyledRect {
required property string modelData
required property int index
width: ListView.view.width
readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
border.width: isCurrent ? 1 : 0
border.color: Colours.palette.m3primary
StateLayer {
function onClicked(): void {
rootPane.fontFamilyMaterial = modelData;
rootPane.saveConfig();
}
}
RowLayout {
id: fontFamilyMaterialRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledText {
text: modelData
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
Loader {
active: isCurrent
asynchronous: true
sourceComponent: MaterialIcon {
text: "check"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
}
}
}
implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2
}
}
}
}
CollapsibleSection {
id: monoFontSection
title: qsTr("Monospace font family")
expanded: false
showBackground: true
nested: true
Loader {
Layout.fillWidth: true
Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0
asynchronous: true
active: monoFontSection.expanded
sourceComponent: StyledListView {
id: monoFontList
property alias contentHeight: monoFontList.contentHeight
clip: true
spacing: Appearance.spacing.small / 2
model: Qt.fontFamilies()
StyledScrollBar.vertical: StyledScrollBar {
flickable: monoFontList
}
delegate: StyledRect {
required property string modelData
required property int index
width: ListView.view.width
readonly property bool isCurrent: modelData === rootPane.fontFamilyMono
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
border.width: isCurrent ? 1 : 0
border.color: Colours.palette.m3primary
StateLayer {
function onClicked(): void {
rootPane.fontFamilyMono = modelData;
rootPane.saveConfig();
}
}
RowLayout {
id: fontFamilyMonoRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledText {
text: modelData
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
Loader {
active: isCurrent
asynchronous: true
sourceComponent: MaterialIcon {
text: "check"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
}
}
}
implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2
}
}
}
}
CollapsibleSection {
id: sansFontSection
title: qsTr("Sans-serif font family")
expanded: false
showBackground: true
nested: true
Loader {
Layout.fillWidth: true
Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0
asynchronous: true
active: sansFontSection.expanded
sourceComponent: StyledListView {
id: sansFontList
property alias contentHeight: sansFontList.contentHeight
clip: true
spacing: Appearance.spacing.small / 2
model: Qt.fontFamilies()
StyledScrollBar.vertical: StyledScrollBar {
flickable: sansFontList
}
delegate: StyledRect {
required property string modelData
required property int index
width: ListView.view.width
readonly property bool isCurrent: modelData === rootPane.fontFamilySans
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
border.width: isCurrent ? 1 : 0
border.color: Colours.palette.m3primary
StateLayer {
function onClicked(): void {
rootPane.fontFamilySans = modelData;
rootPane.saveConfig();
}
}
RowLayout {
id: fontFamilySansRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledText {
text: modelData
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
Loader {
active: isCurrent
asynchronous: true
sourceComponent: MaterialIcon {
text: "check"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
}
}
}
implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2
}
}
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
SliderInput {
Layout.fillWidth: true
label: qsTr("Font size scale")
value: rootPane.fontSizeScale
from: 0.7
to: 1.5
decimals: 2
suffix: "×"
validator: DoubleValidator { bottom: 0.7; top: 1.5 }
onValueModified: (newValue) => {
rootPane.fontSizeScale = newValue;
rootPane.saveConfig();
}
}
}
}
CollapsibleSection {
id: scalesSection
title: qsTr("Scales")
showBackground: true
SectionContainer {
contentSpacing: Appearance.spacing.normal
SliderInput {
Layout.fillWidth: true
label: qsTr("Padding scale")
value: rootPane.paddingScale
from: 0.5
to: 2.0
decimals: 1
suffix: "×"
validator: DoubleValidator { bottom: 0.5; top: 2.0 }
onValueModified: (newValue) => {
rootPane.paddingScale = newValue;
rootPane.saveConfig();
}
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
SliderInput {
Layout.fillWidth: true
label: qsTr("Rounding scale")
value: rootPane.roundingScale
from: 0.1
to: 5.0
decimals: 1
suffix: "×"
validator: DoubleValidator { bottom: 0.1; top: 5.0 }
onValueModified: (newValue) => {
rootPane.roundingScale = newValue;
rootPane.saveConfig();
}
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
SliderInput {
Layout.fillWidth: true
label: qsTr("Spacing scale")
value: rootPane.spacingScale
from: 0.1
to: 2.0
decimals: 1
suffix: "×"
validator: DoubleValidator { bottom: 0.1; top: 2.0 }
onValueModified: (newValue) => {
rootPane.spacingScale = newValue;
rootPane.saveConfig();
}
}
}
}
CollapsibleSection {
id: transparencySection
title: qsTr("Transparency")
showBackground: true
SwitchRow {
label: qsTr("Transparency enabled")
checked: rootPane.transparencyEnabled
onToggled: checked => {
rootPane.transparencyEnabled = checked;
rootPane.saveConfig();
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
SliderInput {
Layout.fillWidth: true
label: qsTr("Transparency base")
value: rootPane.transparencyBase * 100
from: 0
to: 100
suffix: "%"
validator: IntValidator { bottom: 0; top: 100 }
formatValueFunction: (val) => Math.round(val).toString()
parseValueFunction: (text) => parseInt(text)
onValueModified: (newValue) => {
rootPane.transparencyBase = newValue / 100;
rootPane.saveConfig();
}
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
SliderInput {
Layout.fillWidth: true
label: qsTr("Transparency layers")
value: rootPane.transparencyLayers * 100
from: 0
to: 100
suffix: "%"
validator: IntValidator { bottom: 0; top: 100 }
formatValueFunction: (val) => Math.round(val).toString()
parseValueFunction: (text) => parseInt(text)
onValueModified: (newValue) => {
rootPane.transparencyLayers = newValue / 100;
rootPane.saveConfig();
}
}
}
}
CollapsibleSection {
id: borderSection
title: qsTr("Border")
showBackground: true
SectionContainer {
contentSpacing: Appearance.spacing.normal
SliderInput {
Layout.fillWidth: true
label: qsTr("Border rounding")
value: rootPane.borderRounding
from: 0.1
to: 100
decimals: 1
suffix: "px"
validator: DoubleValidator { bottom: 0.1; top: 100 }
onValueModified: (newValue) => {
rootPane.borderRounding = newValue;
rootPane.saveConfig();
}
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
SliderInput {
Layout.fillWidth: true
label: qsTr("Border thickness")
value: rootPane.borderThickness
from: 0.1
to: 100
decimals: 1
suffix: "px"
validator: DoubleValidator { bottom: 0.1; top: 100 }
onValueModified: (newValue) => {
rootPane.borderThickness = newValue;
rootPane.saveConfig();
}
}
}
}
CollapsibleSection {
id: backgroundSection
title: qsTr("Background")
showBackground: true
SwitchRow {
label: qsTr("Desktop clock")
checked: rootPane.desktopClockEnabled
onToggled: checked => {
rootPane.desktopClockEnabled = checked;
rootPane.saveConfig();
}
}
SwitchRow {
label: qsTr("Background enabled")
checked: rootPane.backgroundEnabled
onToggled: checked => {
rootPane.backgroundEnabled = checked;
rootPane.saveConfig();
}
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("Visualiser")
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
SwitchRow {
label: qsTr("Visualiser enabled")
checked: rootPane.visualiserEnabled
onToggled: checked => {
rootPane.visualiserEnabled = checked;
rootPane.saveConfig();
}
}
SwitchRow {
label: qsTr("Visualiser auto hide")
checked: rootPane.visualiserAutoHide
onToggled: checked => {
rootPane.visualiserAutoHide = checked;
rootPane.saveConfig();
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
SliderInput {
Layout.fillWidth: true
label: qsTr("Visualiser rounding")
value: rootPane.visualiserRounding
from: 0
to: 10
stepSize: 1
validator: IntValidator { bottom: 0; top: 10 }
formatValueFunction: (val) => Math.round(val).toString()
parseValueFunction: (text) => parseInt(text)
onValueModified: (newValue) => {
rootPane.visualiserRounding = Math.round(newValue);
rootPane.saveConfig();
}
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
SliderInput {
Layout.fillWidth: true
label: qsTr("Visualiser spacing")
value: rootPane.visualiserSpacing
from: 0
to: 2
validator: DoubleValidator { bottom: 0; top: 2 }
onValueModified: (newValue) => {
rootPane.visualiserSpacing = newValue;
rootPane.saveConfig();
}
}
}
}
}
}
}
rightContent: appearanceRightContentComponent
}
}