summaryrefslogtreecommitdiff
path: root/modules/controlcenter/appearance
diff options
context:
space:
mode:
authorATMDA <atdma2600@gmail.com>2025-11-19 12:39:45 -0500
committerATMDA <atdma2600@gmail.com>2025-11-19 12:39:45 -0500
commitad4213d45ccf227e3528dd2bcb992ec75ab8d0c1 (patch)
tree304fb0aaccd4b830ef8ca5d03c5bfed8fc275062 /modules/controlcenter/appearance
parentcontrolcenter: corrected nesting of bg when transparency off (diff)
downloadcaelestia-shell-ad4213d45ccf227e3528dd2bcb992ec75ab8d0c1.tar.gz
caelestia-shell-ad4213d45ccf227e3528dd2bcb992ec75ab8d0c1.tar.bz2
caelestia-shell-ad4213d45ccf227e3528dd2bcb992ec75ab8d0c1.zip
refactor: SplitPaneLayout now component
Diffstat (limited to 'modules/controlcenter/appearance')
-rw-r--r--modules/controlcenter/appearance/AppearancePane.qml1291
1 files changed, 614 insertions, 677 deletions
diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml
index 61cdcaa..2041bf8 100644
--- a/modules/controlcenter/appearance/AppearancePane.qml
+++ b/modules/controlcenter/appearance/AppearancePane.qml
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import ".."
+import "../components"
import "../../launcher/services"
import qs.components
import qs.components.controls
@@ -16,7 +17,7 @@ import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
-RowLayout {
+Item {
id: root
required property Session session
@@ -46,9 +47,6 @@ RowLayout {
anchors.fill: parent
- spacing: 0
-
-
function saveConfig() {
Config.appearance.anim.durations.scale = root.animDurationsScale;
@@ -79,45 +77,626 @@ RowLayout {
Config.save();
}
- Item {
- id: leftAppearanceItem
- Layout.preferredWidth: Math.floor(parent.width * 0.4)
- Layout.minimumWidth: 420
- Layout.fillHeight: true
+ Component {
+ id: appearanceRightContentComponent
- ClippingRectangle {
- id: leftAppearanceClippingRect
- anchors.fill: parent
- anchors.margins: Appearance.padding.normal
- anchors.leftMargin: 0
- anchors.rightMargin: Appearance.padding.normal / 2
- radius: leftAppearanceBorder.innerRadius
- color: "transparent"
+ StyledFlickable {
+ id: rightAppearanceFlickable
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: contentLayout.height
- Loader {
- id: leftAppearanceLoader
- anchors.fill: parent
- anchors.margins: Appearance.padding.large + Appearance.padding.normal
- anchors.leftMargin: Appearance.padding.large
- anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
- asynchronous: true
- sourceComponent: appearanceLeftContentComponent
- property var rootPane: root
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: rightAppearanceFlickable
}
- }
- InnerBorder {
- id: leftAppearanceBorder
- leftThickness: 0
- rightThickness: Appearance.padding.normal / 2
+ ColumnLayout {
+ id: contentLayout
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ spacing: Appearance.spacing.normal
+
+ MaterialIcon {
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
+ Layout.topMargin: 0
+ text: "palette"
+ font.pointSize: Appearance.font.size.extraLarge * 3
+ font.bold: true
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ text: qsTr("Appearance Settings")
+ font.pointSize: Appearance.font.size.large
+ font.bold: true
+ }
+
+ 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: {
+ // Lazy load: only activate when:
+ // 1. Right pane is loaded AND
+ // 2. Appearance pane is active (index 3) or adjacent (for smooth transitions)
+ // This prevents loading all wallpapers when control center opens but appearance pane isn't visible
+ const isActive = root.session.activeIndex === 3;
+ const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1;
+ // Access loader through SplitPaneLayout's rightLoader
+ 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;
+ // Access timer through wallpaperGrid
+ 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;
+ }
+ }
+
+ // Lazy loading model: loads one image at a time, only when touching bottom
+ // This prevents GridView from creating all delegates at once
+ QtObject {
+ id: lazyModel
+
+ property var sourceList: null
+ property int loadedCount: 0 // Total items available to load
+ property int visibleCount: 0 // Items actually exposed to GridView (only visible + buffer)
+ property int totalCount: 0
+
+ function initialize(list) {
+ sourceList = list;
+ totalCount = list ? list.length : 0;
+ // Start with enough items to fill the initial viewport (~3 rows)
+ 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) {
+ // Always round up to complete rows to avoid incomplete rows in the grid
+ 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))
+
+ // Height based on visible items only - prevents GridView from creating all delegates
+ 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
+
+ // Use ListModel for incremental updates to prevent flashing when new items are added
+ ListModel {
+ id: wallpaperListModel
+ }
+
+ model: wallpaperListModel
+
+ Connections {
+ target: lazyModel
+ function onVisibleCountChanged(): void {
+ if (!lazyModel || !lazyModel.sourceList) return;
+
+ const newCount = lazyModel.visibleCount;
+ const currentCount = wallpaperListModel.count;
+
+ // Only append new items - never remove or replace existing ones
+ 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]});
+ }
+
+ // Preserve scroll position after model update
+ 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);
+
+ // Load more when we're within 1 row of running out of loaded items
+ 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);
+
+ // Load more when we're within 1 row of running out of loaded items
+ 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;
+ });
+ }
+ }
+ }
+ }
+
+
+ // Parent Flickable handles scrolling
+ 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
- Component {
- id: appearanceLeftContentComponent
+ leftContent: Component {
StyledFlickable {
id: sidebarFlickable
- readonly property var rootPane: leftAppearanceLoader.rootPane
+ readonly property var rootPane: root
flickableDirection: Flickable.VerticalFlick
contentHeight: sidebarLayout.height
@@ -1808,651 +2387,9 @@ RowLayout {
}
}
}
- }
}
- }
-
- Item {
- Layout.fillWidth: true
- Layout.fillHeight: true
-
- ClippingRectangle {
- id: rightAppearanceClippingRect
- anchors.fill: parent
- anchors.margins: Appearance.padding.normal
- anchors.leftMargin: 0
- anchors.rightMargin: Appearance.padding.normal / 2
- radius: rightAppearanceBorder.innerRadius
- color: "transparent"
-
- Loader {
- id: rightAppearanceLoader
- anchors.fill: parent
- anchors.margins: Appearance.padding.large * 2
- asynchronous: true
- sourceComponent: appearanceRightContentComponent
- property var rootPane: root
-
- onStatusChanged: {
- if (status === Loader.Error) {
- console.error("[AppearancePane] Right appearance loader error!");
- }
- }
- }
}
- InnerBorder {
- id: rightAppearanceBorder
- leftThickness: Appearance.padding.normal / 2
- }
-
- 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
-
- MaterialIcon {
- Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
- Layout.topMargin: 0
- text: "palette"
- font.pointSize: Appearance.font.size.extraLarge * 3
- font.bold: true
- }
-
- StyledText {
- Layout.alignment: Qt.AlignHCenter
- text: qsTr("Appearance Settings")
- font.pointSize: Appearance.font.size.large
- font.bold: true
- }
-
- 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: {
- // Lazy load: only activate when:
- // 1. Right pane is loaded AND
- // 2. Appearance pane is active (index 3) or adjacent (for smooth transitions)
- // This prevents loading all wallpapers when control center opens but appearance pane isn't visible
- const isActive = root.session.activeIndex === 3;
- const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1;
- const shouldActivate = rightAppearanceLoader.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;
- // Access timer through wallpaperGrid
- if (container && container.wallpaperGrid) {
- if (container.wallpaperGrid.scrollCheckTimer) {
- container.wallpaperGrid.scrollCheckTimer.stop();
- }
- container.wallpaperGrid._expansionInProgress = false;
- }
- }
- }
-
- 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;
- }
- }
-
- // Lazy loading model: loads one image at a time, only when touching bottom
- // This prevents GridView from creating all delegates at once
- QtObject {
- id: lazyModel
-
- property var sourceList: null
- property int loadedCount: 0 // Total items available to load
- property int visibleCount: 0 // Items actually exposed to GridView (only visible + buffer)
- property int totalCount: 0
-
- function initialize(list) {
- sourceList = list;
- totalCount = list ? list.length : 0;
- // Start with enough items to fill the initial viewport (~3 rows)
- 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) {
- // Always round up to complete rows to avoid incomplete rows in the grid
- 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))
-
- // Height based on visible items only - prevents GridView from creating all delegates
- 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
-
- // Use ListModel for incremental updates to prevent flashing when new items are added
- ListModel {
- id: wallpaperListModel
- }
-
- model: wallpaperListModel
-
- Connections {
- target: lazyModel
- function onVisibleCountChanged(): void {
- if (!lazyModel || !lazyModel.sourceList) return;
-
- const newCount = lazyModel.visibleCount;
- const currentCount = wallpaperListModel.count;
-
- // Only append new items - never remove or replace existing ones
- 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]});
- }
-
- // Preserve scroll position after model update
- 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);
-
- // Load more when we're within 1 row of running out of loaded items
- 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);
-
- // Load more when we're within 1 row of running out of loaded items
- 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;
- });
- }
- }
- }
- }
-
-
- // Parent Flickable handles scrolling
- 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;
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
+ rightContent: appearanceRightContentComponent
}
}