diff options
| author | ATMDA <atdma2600@gmail.com> | 2025-11-19 12:39:45 -0500 |
|---|---|---|
| committer | ATMDA <atdma2600@gmail.com> | 2025-11-19 12:39:45 -0500 |
| commit | ad4213d45ccf227e3528dd2bcb992ec75ab8d0c1 (patch) | |
| tree | 304fb0aaccd4b830ef8ca5d03c5bfed8fc275062 /modules/controlcenter | |
| parent | controlcenter: corrected nesting of bg when transparency off (diff) | |
| download | caelestia-shell-ad4213d45ccf227e3528dd2bcb992ec75ab8d0c1.tar.gz caelestia-shell-ad4213d45ccf227e3528dd2bcb992ec75ab8d0c1.tar.bz2 caelestia-shell-ad4213d45ccf227e3528dd2bcb992ec75ab8d0c1.zip | |
refactor: SplitPaneLayout now component
Diffstat (limited to 'modules/controlcenter')
| -rw-r--r-- | modules/controlcenter/appearance/AppearancePane.qml | 1291 | ||||
| -rw-r--r-- | modules/controlcenter/audio/AudioPane.qml | 562 | ||||
| -rw-r--r-- | modules/controlcenter/bluetooth/BtPane.qml | 138 | ||||
| -rw-r--r-- | modules/controlcenter/components/SplitPaneLayout.qml | 120 | ||||
| -rw-r--r-- | modules/controlcenter/launcher/LauncherPane.qml | 288 | ||||
| -rw-r--r-- | modules/controlcenter/network/NetworkingPane.qml | 294 |
6 files changed, 1287 insertions, 1406 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 } } diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index c2d60d8..dc3ba56 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects @@ -11,52 +12,17 @@ import Quickshell.Widgets import QtQuick import QtQuick.Layouts -RowLayout { +Item { id: root required property Session session anchors.fill: parent - spacing: 0 + SplitPaneLayout { + anchors.fill: parent - Item { - id: leftAudioItem - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - ClippingRectangle { - id: leftAudioClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: leftAudioBorder.innerRadius - color: "transparent" - - Loader { - id: leftAudioLoader - - 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: audioLeftContentComponent - } - } - - InnerBorder { - id: leftAudioBorder - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } - - Component { - id: audioLeftContentComponent + leftContent: Component { StyledFlickable { id: leftAudioFlickable @@ -246,349 +212,321 @@ RowLayout { } } } - } } - } - - Item { - id: rightAudioItem - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - id: rightAudioClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: rightAudioBorder.innerRadius - color: "transparent" - - Loader { - id: rightAudioLoader + } + rightContent: Component { + Item { anchors.fill: parent anchors.topMargin: Appearance.padding.large * 2 anchors.bottomMargin: Appearance.padding.large * 2 anchors.leftMargin: 0 anchors.rightMargin: 0 - asynchronous: true - sourceComponent: audioRightContentComponent - } - } - - InnerBorder { - id: rightAudioBorder - leftThickness: Appearance.padding.normal / 2 - } - - Component { - id: audioRightContentComponent + StyledFlickable { + id: rightAudioFlickable + anchors.fill: parent + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height - StyledFlickable { - id: rightAudioFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: rightAudioFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: rightAudioFlickable + } - ColumnLayout { - id: contentLayout + ColumnLayout { + id: contentLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Appearance.padding.large * 2 - anchors.rightMargin: Appearance.padding.large * 2 - spacing: Appearance.spacing.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Appearance.padding.large * 2 + anchors.rightMargin: Appearance.padding.large * 2 + spacing: Appearance.spacing.normal - ConnectionHeader { - icon: "volume_up" - title: qsTr("Audio Settings") - } + ConnectionHeader { + icon: "volume_up" + title: qsTr("Audio Settings") + } - SectionHeader { - title: qsTr("Output volume") - description: qsTr("Control the volume of your output device") - } + SectionHeader { + title: qsTr("Output volume") + description: qsTr("Control the volume of your output device") + } - SectionContainer { - contentSpacing: Appearance.spacing.normal + SectionContainer { + contentSpacing: Appearance.spacing.normal - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - StyledText { - text: qsTr("Volume") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } + StyledText { + text: qsTr("Volume") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: outputVolumeInput.implicitHeight + Appearance.padding.small * 2 - color: outputVolumeInputHover.containsMouse || outputVolumeInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: outputVolumeInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - enabled: !Audio.muted - opacity: enabled ? 1 : 0.5 + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: outputVolumeInput.implicitHeight + Appearance.padding.small * 2 + color: outputVolumeInputHover.containsMouse || outputVolumeInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: outputVolumeInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + enabled: !Audio.muted + opacity: enabled ? 1 : 0.5 - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } - MouseArea { - id: outputVolumeInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } + MouseArea { + id: outputVolumeInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } - StyledTextField { - id: outputVolumeInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } - enabled: !Audio.muted - - Component.onCompleted: { - text = Math.round(Audio.volume * 100).toString(); - } - - Connections { - target: Audio - function onVolumeChanged() { - if (!outputVolumeInput.activeFocus) { - outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); + StyledTextField { + id: outputVolumeInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 100 } + enabled: !Audio.muted + + Component.onCompleted: { + text = Math.round(Audio.volume * 100).toString(); } - } - } - - onTextChanged: { - if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - Audio.setVolume(val / 100); + + Connections { + target: Audio + function onVolumeChanged() { + if (!outputVolumeInput.activeFocus) { + outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); + } + } + } + + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + Audio.setVolume(val / 100); + } + } + } + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(Audio.volume * 100).toString(); + } } } } - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(Audio.volume * 100).toString(); - } + + StyledText { + text: "%" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + opacity: Audio.muted ? 0.5 : 1 } - } - } - StyledText { - text: "%" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - opacity: Audio.muted ? 0.5 : 1 - } + StyledRect { + implicitWidth: implicitHeight + implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2 - StyledRect { - implicitWidth: implicitHeight - implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2 + radius: Appearance.rounding.normal + color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - radius: Appearance.rounding.normal - color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + StateLayer { + function onClicked(): void { + if (Audio.sink?.audio) { + Audio.sink.audio.muted = !Audio.sink.audio.muted; + } + } + } - StateLayer { - function onClicked(): void { - if (Audio.sink?.audio) { - Audio.sink.audio.muted = !Audio.sink.audio.muted; + MaterialIcon { + id: muteIcon + + anchors.centerIn: parent + text: Audio.muted ? "volume_off" : "volume_up" + color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer } } } - MaterialIcon { - id: muteIcon + StyledSlider { + id: outputVolumeSlider + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 - anchors.centerIn: parent - text: Audio.muted ? "volume_off" : "volume_up" - color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + value: Audio.volume + enabled: !Audio.muted + opacity: enabled ? 1 : 0.5 + onMoved: { + Audio.setVolume(value); + if (!outputVolumeInput.activeFocus) { + outputVolumeInput.text = Math.round(value * 100).toString(); + } + } } } } - StyledSlider { - id: outputVolumeSlider - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - value: Audio.volume - enabled: !Audio.muted - opacity: enabled ? 1 : 0.5 - onMoved: { - Audio.setVolume(value); - if (!outputVolumeInput.activeFocus) { - outputVolumeInput.text = Math.round(value * 100).toString(); - } - } + SectionHeader { + title: qsTr("Input volume") + description: qsTr("Control the volume of your input device") } - } - } - SectionHeader { - title: qsTr("Input volume") - description: qsTr("Control the volume of your input device") - } + SectionContainer { + contentSpacing: Appearance.spacing.normal - SectionContainer { - contentSpacing: Appearance.spacing.normal - - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - StyledText { - text: qsTr("Volume") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } + StyledText { + text: qsTr("Volume") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: inputVolumeInput.implicitHeight + Appearance.padding.small * 2 - color: inputVolumeInputHover.containsMouse || inputVolumeInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: inputVolumeInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - enabled: !Audio.sourceMuted - opacity: enabled ? 1 : 0.5 + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: inputVolumeInput.implicitHeight + Appearance.padding.small * 2 + color: inputVolumeInputHover.containsMouse || inputVolumeInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: inputVolumeInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + enabled: !Audio.sourceMuted + opacity: enabled ? 1 : 0.5 - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } - MouseArea { - id: inputVolumeInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } + MouseArea { + id: inputVolumeInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } - StyledTextField { - id: inputVolumeInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } - enabled: !Audio.sourceMuted - - Component.onCompleted: { - text = Math.round(Audio.sourceVolume * 100).toString(); - } - - Connections { - target: Audio - function onSourceVolumeChanged() { - if (!inputVolumeInput.activeFocus) { - inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); + StyledTextField { + id: inputVolumeInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 100 } + enabled: !Audio.sourceMuted + + Component.onCompleted: { + text = Math.round(Audio.sourceVolume * 100).toString(); } - } - } - - onTextChanged: { - if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - Audio.setSourceVolume(val / 100); + + Connections { + target: Audio + function onSourceVolumeChanged() { + if (!inputVolumeInput.activeFocus) { + inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); + } + } + } + + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + Audio.setSourceVolume(val / 100); + } + } + } + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(Audio.sourceVolume * 100).toString(); + } } } } - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(Audio.sourceVolume * 100).toString(); - } - } - } - } - StyledText { - text: "%" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - opacity: Audio.sourceMuted ? 0.5 : 1 - } + StyledText { + text: "%" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + opacity: Audio.sourceMuted ? 0.5 : 1 + } - StyledRect { - implicitWidth: implicitHeight - implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.normal - color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + radius: Appearance.rounding.normal + color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - StateLayer { - function onClicked(): void { - if (Audio.source?.audio) { - Audio.source.audio.muted = !Audio.source.audio.muted; + StateLayer { + function onClicked(): void { + if (Audio.source?.audio) { + Audio.source.audio.muted = !Audio.source.audio.muted; + } + } } - } - } - MaterialIcon { - id: muteInputIcon + MaterialIcon { + id: muteInputIcon - anchors.centerIn: parent - text: "mic_off" - color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + anchors.centerIn: parent + text: "mic_off" + color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + } + } } - } - } - StyledSlider { - id: inputVolumeSlider - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + StyledSlider { + id: inputVolumeSlider + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 - value: Audio.sourceVolume - enabled: !Audio.sourceMuted - opacity: enabled ? 1 : 0.5 - onMoved: { - Audio.setSourceVolume(value); - if (!inputVolumeInput.activeFocus) { - inputVolumeInput.text = Math.round(value * 100).toString(); + value: Audio.sourceVolume + enabled: !Audio.sourceMuted + opacity: enabled ? 1 : 0.5 + onMoved: { + Audio.setSourceVolume(value); + if (!inputVolumeInput.activeFocus) { + inputVolumeInput.text = Math.round(value * 100).toString(); + } + } } } } } } } - } } } }
\ No newline at end of file diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 8ad4b1f..cacb611 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components.controls import qs.components.effects import qs.components.containers @@ -10,95 +11,50 @@ import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts -RowLayout { +Item { id: root required property Session session anchors.fill: parent - spacing: 0 - - Item { - id: leftBtItem - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - ClippingRectangle { - id: leftBtClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: leftBtBorder.innerRadius - color: "transparent" - - Loader { - id: leftBtLoader - - 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: btDeviceListComponent - } - } - - InnerBorder { - id: leftBtBorder - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } - - Component { - id: btDeviceListComponent + SplitPaneLayout { + anchors.fill: parent + leftContent: Component { DeviceList { anchors.fill: parent session: root.session } } - } - Item { - id: rightBtItem - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - id: btClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: rightBorder.innerRadius - color: "transparent" - - Loader { - id: loader + rightContent: Component { + Item { + id: rightBtPane property BluetoothDevice pane: root.session.bt.active - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 + Loader { + id: rightLoader - asynchronous: true - sourceComponent: pane ? details : settings + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + asynchronous: true + sourceComponent: rightBtPane.pane ? details : settings + } Behavior on pane { SequentialAnimation { ParallelAnimation { Anim { + target: rightLoader property: "opacity" to: 0 easing.bezierCurve: Appearance.anim.curves.standardAccel } Anim { + target: rightLoader property: "scale" to: 0.8 easing.bezierCurve: Appearance.anim.curves.standardAccel @@ -107,11 +63,13 @@ RowLayout { PropertyAction {} ParallelAnimation { Anim { + target: rightLoader property: "opacity" to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { + target: rightLoader property: "scale" to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel @@ -119,49 +77,49 @@ RowLayout { } } } - } - } - InnerBorder { - id: rightBorder - - leftThickness: Appearance.padding.normal / 2 + Connections { + target: root.session.bt + function onActiveChanged() { + rightBtPane.pane = root.session.bt.active; + } + } + } } + } - Component { - id: settings + Component { + id: settings - StyledFlickable { - id: settingsFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height + StyledFlickable { + id: settingsFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } - Settings { - id: settingsInner + Settings { + id: settingsInner - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session } } + } - Component { - id: details + Component { + id: details - Details { - session: root.session - } + Details { + session: root.session } } component Anim: NumberAnimation { - target: loader duration: Appearance.anim.durations.normal / 2 easing.type: Easing.BezierSpline } diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml new file mode 100644 index 0000000..7bd7db0 --- /dev/null +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -0,0 +1,120 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.effects +import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + spacing: 0 + + property Component leftContent: null + property Component rightContent: null + + // Left pane configuration + property real leftWidthRatio: 0.4 + property int leftMinimumWidth: 420 + property var leftLoaderProperties: ({}) + + // Right pane configuration + property var rightLoaderProperties: ({}) + + // Expose loaders for customization (access via splitLayout.leftLoader or splitLayout.rightLoader) + property alias leftLoader: leftLoader + property alias rightLoader: rightLoader + + // Left pane + Item { + id: leftPane + + Layout.preferredWidth: Math.floor(parent.width * root.leftWidthRatio) + Layout.minimumWidth: root.leftMinimumWidth + Layout.fillHeight: true + + ClippingRectangle { + id: leftClippingRect + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + + radius: leftBorder.innerRadius + color: "transparent" + + Loader { + id: leftLoader + + 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: root.leftContent + + // Apply any additional properties from leftLoaderProperties + Component.onCompleted: { + for (const key in root.leftLoaderProperties) { + leftLoader[key] = root.leftLoaderProperties[key]; + } + } + } + } + + InnerBorder { + id: leftBorder + + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + // Right pane + Item { + id: rightPane + + Layout.fillWidth: true + Layout.fillHeight: true + + ClippingRectangle { + id: rightClippingRect + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + + radius: rightBorder.innerRadius + color: "transparent" + + Loader { + id: rightLoader + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + asynchronous: true + sourceComponent: root.rightContent + + // Apply any additional properties from rightLoaderProperties + Component.onCompleted: { + for (const key in root.rightLoaderProperties) { + rightLoader[key] = root.rightLoaderProperties[key]; + } + } + } + } + + InnerBorder { + id: rightBorder + + leftThickness: Appearance.padding.normal / 2 + } + } +} + diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index f2247a7..30e2953 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.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 QtQuick import QtQuick.Layouts import "../../../utils/scripts/fuzzysort.js" as Fuzzy -RowLayout { +Item { id: root required property Session session @@ -26,8 +27,6 @@ RowLayout { anchors.fill: parent - spacing: 0 - onSelectedAppChanged: { root.session.launcher.active = root.selectedApp; updateToggleState(); @@ -156,43 +155,10 @@ RowLayout { } } - Item { - id: leftLauncherItem - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - ClippingRectangle { - id: leftLauncherClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: leftLauncherBorder.innerRadius - color: "transparent" - - Loader { - id: leftLauncherLoader - - 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: leftContentComponent - } - } - - InnerBorder { - id: leftLauncherBorder - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } + SplitPaneLayout { + anchors.fill: parent - Component { - id: leftContentComponent + leftContent: Component { ColumnLayout { id: leftLauncherLayout @@ -336,7 +302,8 @@ RowLayout { // Lazy load: activate when left pane is loaded // The ListView will load asynchronously, and search will work because filteredApps // is updated regardless of whether the ListView is loaded - return leftLauncherLoader.item !== null; + // Access loader through parent - this will be set when component loads + return true; } sourceComponent: StyledListView { @@ -412,25 +379,10 @@ RowLayout { } } } - } - - Item { - id: rightLauncherItem - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - id: rightLauncherClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - radius: rightLauncherBorder.innerRadius - color: "transparent" - - Loader { - id: rightLauncherLoader + rightContent: Component { + Item { + id: rightLauncherPane property var pane: root.session.launcher.active property string paneId: pane ? (pane.id || pane.entry?.id || "") : "" @@ -442,28 +394,34 @@ RowLayout { return pane ? appDetails : settings; } - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - - opacity: 1 - scale: 1 - transformOrigin: Item.Center - clip: false - - asynchronous: true - sourceComponent: rightLauncherLoader.targetComponent - active: true - Component.onCompleted: { displayedApp = pane; targetComponent = getComponentForPane(); nextComponent = targetComponent; } - onItemChanged: { - // Ensure displayedApp is set when item is created (for async loading) - if (item && pane && displayedApp !== pane) { - displayedApp = pane; + Loader { + id: rightLauncherLoader + + anchors.fill: parent + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + clip: false + + asynchronous: true + sourceComponent: rightLauncherPane.targetComponent + active: true + + // Expose displayedApp to loaded components + property var displayedApp: rightLauncherPane.displayedApp + + onItemChanged: { + // Ensure displayedApp is set when item is created (for async loading) + if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) { + rightLauncherPane.displayedApp = rightLauncherPane.pane; + } } } @@ -484,9 +442,9 @@ RowLayout { } } PropertyAction { - target: rightLauncherLoader + target: rightLauncherPane property: "displayedApp" - value: rightLauncherLoader.pane + value: rightLauncherPane.pane } PropertyAction { target: rightLauncherLoader @@ -494,9 +452,9 @@ RowLayout { value: false } PropertyAction { - target: rightLauncherLoader + target: rightLauncherPane property: "targetComponent" - value: rightLauncherLoader.nextComponent + value: rightLauncherPane.nextComponent } PropertyAction { target: rightLauncherLoader @@ -539,90 +497,90 @@ RowLayout { } } } + } - InnerBorder { - id: rightLauncherBorder - - leftThickness: Appearance.padding.normal / 2 - } - - Component { - id: settings + Component { + id: settings - StyledFlickable { - id: settingsFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height + StyledFlickable { + id: settingsFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } - Settings { - id: settingsInner + Settings { + id: settingsInner - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session } } + } - Component { - id: appDetails + Component { + id: appDetails - ColumnLayout { - anchors.fill: parent + ColumnLayout { + id: appDetailsLayout + anchors.fill: parent - spacing: Appearance.spacing.normal + // Get displayedApp from parent Loader (the Loader has displayedApp property we set) + readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null - Item { - Layout.alignment: Qt.AlignHCenter - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 - Layout.topMargin: Appearance.padding.large * 2 - implicitWidth: iconLoader.implicitWidth - implicitHeight: iconLoader.implicitHeight + spacing: Appearance.spacing.normal - Loader { - id: iconLoader - sourceComponent: rightLauncherLoader.displayedApp ? appIconComponent : defaultIconComponent - } + Item { + Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: Appearance.padding.large * 2 + Layout.rightMargin: Appearance.padding.large * 2 + Layout.topMargin: Appearance.padding.large * 2 + implicitWidth: iconLoader.implicitWidth + implicitHeight: iconLoader.implicitHeight - Component { - id: appIconComponent - IconImage { - implicitSize: Appearance.font.size.extraLarge * 3 * 2 - source: { - if (!rightLauncherLoader.displayedApp) return "image-missing"; - const entry = rightLauncherLoader.displayedApp.entry; - if (entry && entry.icon) { - return Quickshell.iconPath(entry.icon, "image-missing"); - } - return "image-missing"; + Loader { + id: iconLoader + sourceComponent: parent.parent.displayedApp ? appIconComponent : defaultIconComponent + } + + Component { + id: appIconComponent + IconImage { + implicitSize: Appearance.font.size.extraLarge * 3 * 2 + source: { + const app = iconLoader.parent.parent.displayedApp; + if (!app) return "image-missing"; + const entry = app.entry; + if (entry && entry.icon) { + return Quickshell.iconPath(entry.icon, "image-missing"); } + return "image-missing"; } } + } - Component { - id: defaultIconComponent - MaterialIcon { - text: "apps" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } + Component { + id: defaultIconComponent + MaterialIcon { + text: "apps" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true } } + } - StyledText { - Layout.alignment: Qt.AlignHCenter - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 - text: rightLauncherLoader.displayedApp ? (rightLauncherLoader.displayedApp.name || rightLauncherLoader.displayedApp.entry?.name || qsTr("Application Details")) : qsTr("Launcher Applications") - font.pointSize: Appearance.font.size.large - font.bold: true - } + StyledText { + Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: Appearance.padding.large * 2 + Layout.rightMargin: Appearance.padding.large * 2 + text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : qsTr("Launcher Applications") + font.pointSize: Appearance.font.size.large + font.bold: true + } Item { Layout.fillWidth: true @@ -648,38 +606,38 @@ RowLayout { anchors.top: parent.top spacing: Appearance.spacing.normal - SwitchRow { - Layout.topMargin: Appearance.spacing.normal - visible: rightLauncherLoader.displayedApp !== null - label: qsTr("Hide from launcher") - checked: root.hideFromLauncherChecked - enabled: rightLauncherLoader.displayedApp !== null - onToggled: checked => { - root.hideFromLauncherChecked = checked; - if (rightLauncherLoader.displayedApp) { - const appId = rightLauncherLoader.displayedApp.id || rightLauncherLoader.displayedApp.entry?.id; - const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; - if (checked) { - if (!hiddenApps.includes(appId)) { - hiddenApps.push(appId); - } - } else { - const index = hiddenApps.indexOf(appId); - if (index !== -1) { - hiddenApps.splice(index, 1); - } + SwitchRow { + Layout.topMargin: Appearance.spacing.normal + visible: appDetailsLayout.displayedApp !== null + label: qsTr("Hide from launcher") + checked: root.hideFromLauncherChecked + enabled: appDetailsLayout.displayedApp !== null + onToggled: checked => { + root.hideFromLauncherChecked = checked; + const app = appDetailsLayout.displayedApp; + if (app) { + const appId = app.id || app.entry?.id; + const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; + if (checked) { + if (!hiddenApps.includes(appId)) { + hiddenApps.push(appId); + } + } else { + const index = hiddenApps.indexOf(appId); + if (index !== -1) { + hiddenApps.splice(index, 1); } - Config.launcher.hiddenApps = hiddenApps; - Config.save(); } + Config.launcher.hiddenApps = hiddenApps; + Config.save(); } } + } } } } } - } } component Anim: NumberAnimation { diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index d0ea852..55c70d2 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import "." import qs.components import qs.components.controls @@ -21,49 +22,12 @@ Item { anchors.fill: parent - RowLayout { - id: contentLayout + SplitPaneLayout { + id: splitLayout anchors.fill: parent - spacing: 0 - Item { - id: leftNetworkItem - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - ClippingRectangle { - id: leftNetworkClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: leftNetworkBorder.innerRadius - color: "transparent" - - Loader { - id: leftNetworkLoader - - 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: networkListComponent - } - } - - InnerBorder { - id: leftNetworkBorder - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } - - Component { - id: networkListComponent + leftContent: Component { StyledFlickable { id: leftFlickable @@ -473,38 +437,47 @@ Item { } } } - } } - Item { - id: rightNetworkItem - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - id: networkClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 + rightContent: Component { + Item { + id: rightPaneItem + + // Right pane - networking details/settings + property var ethernetPane: root.session.ethernet.active + property var wirelessPane: root.session.network.active + property var pane: ethernetPane || wirelessPane + property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "") + property Component targetComponent: settings + property Component nextComponent: settings - radius: rightBorder.innerRadius - color: "transparent" + function getComponentForPane() { + return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings; + } - // Right pane - networking details/settings - Loader { - id: loader + Component.onCompleted: { + targetComponent = getComponentForPane(); + nextComponent = targetComponent; + } - property var ethernetPane: root.session.ethernet.active - property var wirelessPane: root.session.network.active - property var pane: ethernetPane || wirelessPane - property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "") - property Component targetComponent: settings - property Component nextComponent: settings + Connections { + target: root.session.ethernet + function onActiveChanged() { + nextComponent = getComponentForPane(); + paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + } + } - function getComponentForPane() { - return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings; + Connections { + target: root.session.network + function onActiveChanged() { + nextComponent = getComponentForPane(); + paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); } + } + + Loader { + id: rightLoader anchors.fill: parent anchors.margins: Appearance.padding.large * 2 @@ -515,131 +488,129 @@ Item { clip: false asynchronous: true - sourceComponent: loader.targetComponent + sourceComponent: rightPaneItem.targetComponent - Component.onCompleted: { - targetComponent = getComponentForPane(); - nextComponent = targetComponent; + Connections { + target: rightPaneItem + function onPaneIdChanged() { + rightPaneItem.targetComponent = rightPaneItem.nextComponent; + } } + } - Behavior on paneId { - SequentialAnimation { - ParallelAnimation { - Anim { - target: loader - property: "opacity" - to: 0 - easing.bezierCurve: Appearance.anim.curves.standardAccel - } - Anim { - target: loader - property: "scale" - to: 0.8 - easing.bezierCurve: Appearance.anim.curves.standardAccel - } + Behavior on paneId { + SequentialAnimation { + ParallelAnimation { + Anim { + target: rightLoader + property: "opacity" + to: 0 + easing.bezierCurve: Appearance.anim.curves.standardAccel } - PropertyAction { - target: loader - property: "targetComponent" - value: loader.nextComponent + Anim { + target: rightLoader + property: "scale" + to: 0.8 + easing.bezierCurve: Appearance.anim.curves.standardAccel } - ParallelAnimation { - Anim { - target: loader - property: "opacity" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - Anim { - target: loader - property: "scale" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } + } + PropertyAction { + target: rightPaneItem + property: "targetComponent" + value: rightPaneItem.nextComponent + } + ParallelAnimation { + Anim { + target: rightLoader + property: "opacity" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + target: rightLoader + property: "scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel } } } + } - onPaneChanged: { - nextComponent = getComponentForPane(); - paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + Connections { + target: rightPaneItem + function onPaneIdChanged() { + rightPaneItem.targetComponent = rightPaneItem.nextComponent; } } } + } + } - InnerBorder { - id: rightBorder - - leftThickness: Appearance.padding.normal / 2 - } - - Component { - id: settings + Component { + id: settings - StyledFlickable { - id: settingsFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height + StyledFlickable { + id: settingsFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } - NetworkSettings { - id: settingsInner + NetworkSettings { + id: settingsInner - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } - } + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session } + } + } - Component { - id: ethernetDetails + Component { + id: ethernetDetails - StyledFlickable { - id: ethernetFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: ethernetDetailsInner.height + StyledFlickable { + id: ethernetFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: ethernetDetailsInner.height - StyledScrollBar.vertical: StyledScrollBar { - flickable: ethernetFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: ethernetFlickable + } - EthernetDetails { - id: ethernetDetailsInner + EthernetDetails { + id: ethernetDetailsInner - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } - } + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session } + } + } - Component { - id: wirelessDetails + Component { + id: wirelessDetails - StyledFlickable { - id: wirelessFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: wirelessDetailsInner.height + StyledFlickable { + id: wirelessFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: wirelessDetailsInner.height - StyledScrollBar.vertical: StyledScrollBar { - flickable: wirelessFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: wirelessFlickable + } - WirelessDetails { - id: wirelessDetailsInner + WirelessDetails { + id: wirelessDetailsInner - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } - } + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session } } } @@ -651,7 +622,6 @@ Item { } component Anim: NumberAnimation { - target: loader duration: Appearance.anim.durations.normal / 2 easing.type: Easing.BezierSpline } |