From ad4213d45ccf227e3528dd2bcb992ec75ab8d0c1 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 12:39:45 -0500 Subject: refactor: SplitPaneLayout now component --- .../controlcenter/appearance/AppearancePane.qml | 3205 ++++++++++---------- modules/controlcenter/audio/AudioPane.qml | 570 ++-- modules/controlcenter/bluetooth/BtPane.qml | 138 +- .../controlcenter/components/SplitPaneLayout.qml | 120 + modules/controlcenter/launcher/LauncherPane.qml | 288 +- modules/controlcenter/network/NetworkingPane.qml | 296 +- 6 files changed, 2249 insertions(+), 2368 deletions(-) create mode 100644 modules/controlcenter/components/SplitPaneLayout.qml (limited to 'modules') 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,794 +77,920 @@ RowLayout { Config.save(); } - Item { - id: leftAppearanceItem - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - 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" - - 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 - } - } + Component { + id: appearanceRightContentComponent - InnerBorder { - id: leftAppearanceBorder - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } + StyledFlickable { + id: rightAppearanceFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height - Component { - id: appearanceLeftContentComponent + StyledScrollBar.vertical: StyledScrollBar { + flickable: rightAppearanceFlickable + } - StyledFlickable { - id: sidebarFlickable - readonly property var rootPane: leftAppearanceLoader.rootPane - flickableDirection: Flickable.VerticalFlick - contentHeight: sidebarLayout.height + ColumnLayout { + id: contentLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal - StyledScrollBar.vertical: StyledScrollBar { - flickable: sidebarFlickable + MaterialIcon { + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.topMargin: 0 + text: "palette" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true } - ColumnLayout { - id: sidebarLayout - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small - - readonly property bool allSectionsExpanded: - themeModeSection.expanded && - colorVariantSection.expanded && - colorSchemeSection.expanded && - animationsSection.expanded && - fontsSection.expanded && - scalesSection.expanded && - transparencySection.expanded && - borderSection.expanded && - backgroundSection.expanded - - RowLayout { - spacing: Appearance.spacing.smaller - - StyledText { - text: qsTr("Appearance") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - IconButton { - icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more" - type: IconButton.Text - label.animate: true - onClicked: { - const shouldExpand = !sidebarLayout.allSectionsExpanded; - themeModeSection.expanded = shouldExpand; - colorVariantSection.expanded = shouldExpand; - colorSchemeSection.expanded = shouldExpand; - animationsSection.expanded = shouldExpand; - fontsSection.expanded = shouldExpand; - scalesSection.expanded = shouldExpand; - transparencySection.expanded = shouldExpand; - borderSection.expanded = shouldExpand; - backgroundSection.expanded = shouldExpand; - } - } + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Appearance Settings") + font.pointSize: Appearance.font.size.large + font.bold: true } - CollapsibleSection { - id: themeModeSection - title: qsTr("Theme mode") - description: qsTr("Light or dark theme") - showBackground: true - - SwitchRow { - label: qsTr("Dark mode") - checked: !Colours.currentLight - onToggled: checked => { - Colours.setMode(checked ? "dark" : "light"); - } - } + StyledText { + Layout.topMargin: Appearance.spacing.large + Layout.alignment: Qt.AlignHCenter + text: qsTr("Wallpaper") + font.pointSize: Appearance.font.size.extraLarge + font.weight: 600 } - CollapsibleSection { - id: colorVariantSection - title: qsTr("Color variant") - description: qsTr("Material theme variant") - showBackground: true - - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small / 2 - - Repeater { - model: M3Variants.list - - delegate: StyledRect { - required property var modelData - - Layout.fillWidth: true - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - const variant = modelData.variant; - - // Optimistic update - set immediately for responsive UI - Schemes.currentVariant = variant; - Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Select a wallpaper") + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } - // Reload after a delay to confirm changes - Qt.callLater(() => { - reloadTimer.restart(); - }); + 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(); } } - - Timer { - id: reloadTimer - interval: 300 - onTriggered: { - Schemes.reload(); + } + } + + 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; } - - RowLayout { - id: variantRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - - MaterialIcon { - text: modelData.icon - font.pointSize: Appearance.font.size.large - fill: modelData.variant === Schemes.currentVariant ? 1 : 0 + return null; + } + + // Cleanup when component is destroyed + Component.onDestruction: { + if (wallpaperGrid) { + if (wallpaperGrid.scrollCheckTimer) { + wallpaperGrid.scrollCheckTimer.stop(); } - - StyledText { - Layout.fillWidth: true - text: modelData.name - font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 + 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; } - - MaterialIcon { - visible: modelData.variant === Schemes.currentVariant - text: "check" - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large + } + + 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; } } - - implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 } - } - } - } - - CollapsibleSection { - id: colorSchemeSection - title: qsTr("Color scheme") - description: qsTr("Available color schemes") - showBackground: true - - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small / 2 - - Repeater { - model: Schemes.list - - delegate: StyledRect { - required property var modelData - - Layout.fillWidth: true - - readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` - readonly property bool isCurrent: schemeKey === Schemes.currentScheme - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - const name = modelData.name; - const flavour = modelData.flavour; - const schemeKey = `${name} ${flavour}`; - - // Optimistic update - set immediately for responsive UI - Schemes.currentScheme = schemeKey; - Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - - // Reload after a delay to confirm changes - Qt.callLater(() => { - reloadTimer.restart(); - }); + + 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: reloadTimer - interval: 300 + id: scrollCheckTimer + interval: 100 + running: { + const isActive = root.session.activeIndex === 3; + return isActive && lazyModel && lazyModel.sourceList && lazyModel.loadedCount < lazyModel.totalCount; + } + repeat: true onTriggered: { - Schemes.reload(); + // 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 - RowLayout { - id: schemeRow - - anchors.fill: parent - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - StyledRect { - id: preview + delegate: Item { + required property var modelData - Layout.alignment: Qt.AlignVCenter + width: wallpaperGrid.cellWidth + height: wallpaperGrid.cellHeight - border.width: 1 - border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) + 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++; + } - color: `#${modelData.colours?.surface}` - radius: Appearance.rounding.full - implicitWidth: iconPlaceholder.implicitWidth - implicitHeight: iconPlaceholder.implicitWidth + StateLayer { + anchors.fill: parent + anchors.leftMargin: itemMargin + anchors.rightMargin: itemMargin + anchors.topMargin: itemMargin + anchors.bottomMargin: itemMargin + radius: itemRadius - MaterialIcon { - id: iconPlaceholder - visible: false - text: "circle" - font.pointSize: Appearance.font.size.large - } + function onClicked(): void { + Wallpapers.setWallpaper(modelData.path); + } + } - Item { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right + StyledClippingRect { + id: image - implicitWidth: parent.implicitWidth / 2 - clip: true + 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 - StyledRect { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right + CachingImage { + id: cachingImage - implicitWidth: preview.implicitWidth - color: `#${modelData.colours?.primary}` - radius: Appearance.rounding.full - } - } - } + path: modelData.path + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + cache: true + visible: opacity > 0 + antialiasing: true + smooth: true - Column { - Layout.fillWidth: true - spacing: 0 + opacity: status === Image.Ready ? 1 : 0 - StyledText { - text: modelData.flavour ?? "" - font.pointSize: Appearance.font.size.normal - } + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutQuad + } + } + } - StyledText { - text: modelData.name ?? "" - font.pointSize: Appearance.font.size.small - color: Colours.palette.m3outline + // Fallback if CachingImage fails to load + Image { + id: fallbackImage - elide: Text.ElideRight - anchors.left: parent.left - anchors.right: parent.right - } - } + 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 - Loader { - active: isCurrent - asynchronous: true + opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutQuad } } + } - implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 + Timer { + id: fallbackTimer + + property bool triggered: false + interval: 800 + running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null + onTriggered: triggered = true } - } - } - } - CollapsibleSection { - id: animationsSection - title: qsTr("Animations") - showBackground: true + // Gradient overlay for filename + Rectangle { + id: filenameOverlay - SectionContainer { - contentSpacing: Appearance.spacing.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + 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) + } + } - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + opacity: 0 - StyledText { - text: qsTr("Animation duration scale") - font.pointSize: Appearance.font.size.normal + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic + } } - Item { - Layout.fillWidth: true + Component.onCompleted: { + opacity = 1; } + } + } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: animDurationsInput.implicitHeight + Appearance.padding.small * 2 - color: animDurationsInputHover.containsMouse || animDurationsInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: animDurationsInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) + 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 color { CAnim {} } - Behavior on border.color { CAnim {} } + Behavior on border.width { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } - MouseArea { - id: animDurationsInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } + MaterialIcon { + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Appearance.padding.small - StyledTextField { - id: animDurationsInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 5.0 } - - Component.onCompleted: { - text = (rootPane.animDurationsScale).toFixed(1); - } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 5.0) { - rootPane.animDurationsScale = val; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 5.0) { - text = (rootPane.animDurationsScale).toFixed(1); - } - } - } - } + visible: isCurrent + text: "check_circle" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.large + } + } - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } + 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; } - StyledSlider { - id: animDurationsSlider + 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 - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + opacity: 0 - from: 0.1 - to: 5.0 - value: rootPane.animDurationsScale - onMoved: { - rootPane.animDurationsScale = animDurationsSlider.value; - if (!animDurationsInput.activeFocus) { - animDurationsInput.text = (animDurationsSlider.value).toFixed(1); - } - rootPane.saveConfig(); + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic } } + + Component.onCompleted: { + opacity = 1; + } + } + } + } } } } + } + } + } - CollapsibleSection { - id: fontsSection - title: qsTr("Fonts") - showBackground: true - - CollapsibleSection { - id: materialFontSection - title: qsTr("Material font family") - expanded: true - showBackground: true - nested: true - - Loader { - id: materialFontLoader - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: materialFontSection.expanded - - sourceComponent: StyledListView { - id: materialFontList - property alias contentHeight: materialFontList.contentHeight - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() - - StyledScrollBar.vertical: StyledScrollBar { - flickable: materialFontList - } - - delegate: StyledRect { - required property string modelData - required property int index + SplitPaneLayout { + anchors.fill: parent - width: ListView.view.width + leftContent: Component { - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + StyledFlickable { + id: sidebarFlickable + readonly property var rootPane: root + flickableDirection: Flickable.VerticalFlick + contentHeight: sidebarLayout.height - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMaterial = modelData; - rootPane.saveConfig(); - } - } - RowLayout { - id: fontFamilyMaterialRow + StyledScrollBar.vertical: StyledScrollBar { + flickable: sidebarFlickable + } - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + ColumnLayout { + id: sidebarLayout + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small - spacing: Appearance.spacing.normal + readonly property bool allSectionsExpanded: + themeModeSection.expanded && + colorVariantSection.expanded && + colorSchemeSection.expanded && + animationsSection.expanded && + fontsSection.expanded && + scalesSection.expanded && + transparencySection.expanded && + borderSection.expanded && + backgroundSection.expanded - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + RowLayout { + spacing: Appearance.spacing.smaller - Item { - Layout.fillWidth: true - } + StyledText { + text: qsTr("Appearance") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } - Loader { - active: isCurrent - asynchronous: true + Item { + Layout.fillWidth: true + } - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } - } - } + IconButton { + icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more" + type: IconButton.Text + label.animate: true + onClicked: { + const shouldExpand = !sidebarLayout.allSectionsExpanded; + themeModeSection.expanded = shouldExpand; + colorVariantSection.expanded = shouldExpand; + colorSchemeSection.expanded = shouldExpand; + animationsSection.expanded = shouldExpand; + fontsSection.expanded = shouldExpand; + scalesSection.expanded = shouldExpand; + transparencySection.expanded = shouldExpand; + borderSection.expanded = shouldExpand; + backgroundSection.expanded = shouldExpand; + } + } + } - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 - } - } + CollapsibleSection { + id: themeModeSection + title: qsTr("Theme mode") + description: qsTr("Light or dark theme") + showBackground: true + SwitchRow { + label: qsTr("Dark mode") + checked: !Colours.currentLight + onToggled: checked => { + Colours.setMode(checked ? "dark" : "light"); } } + } - CollapsibleSection { - id: monoFontSection - title: qsTr("Monospace font family") - expanded: false - showBackground: true - nested: true + CollapsibleSection { + id: colorVariantSection + title: qsTr("Color variant") + description: qsTr("Material theme variant") + showBackground: true - Loader { - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: monoFontSection.expanded + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small / 2 - sourceComponent: StyledListView { - id: monoFontList - property alias contentHeight: monoFontList.contentHeight + Repeater { + model: M3Variants.list - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() + delegate: StyledRect { + required property var modelData - StyledScrollBar.vertical: StyledScrollBar { - flickable: monoFontList - } + Layout.fillWidth: true - delegate: StyledRect { - required property string modelData - required property int index + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 + border.color: Colours.palette.m3primary - width: ListView.view.width + StateLayer { + function onClicked(): void { + const variant = modelData.variant; - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + // Optimistic update - set immediately for responsive UI + Schemes.currentVariant = variant; + Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMono = modelData; - rootPane.saveConfig(); - } + // Reload after a delay to confirm changes + Qt.callLater(() => { + reloadTimer.restart(); + }); + } + } + + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); } + } - RowLayout { - id: fontFamilyMonoRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal + RowLayout { + id: variantRow - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - Item { - Layout.fillWidth: true - } + spacing: Appearance.spacing.normal - Loader { - active: isCurrent - asynchronous: true + MaterialIcon { + text: modelData.icon + font.pointSize: Appearance.font.size.large + fill: modelData.variant === Schemes.currentVariant ? 1 : 0 + } - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } - } + StyledText { + Layout.fillWidth: true + text: modelData.name + font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 } - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + MaterialIcon { + visible: modelData.variant === Schemes.currentVariant + text: "check" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.large + } } + + implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 } } } + } - CollapsibleSection { - id: sansFontSection - title: qsTr("Sans-serif font family") - expanded: false - showBackground: true - nested: true - - Loader { - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: sansFontSection.expanded - - sourceComponent: StyledListView { - id: sansFontList - property alias contentHeight: sansFontList.contentHeight - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() + CollapsibleSection { + id: colorSchemeSection + title: qsTr("Color scheme") + description: qsTr("Available color schemes") + showBackground: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: sansFontList - } + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small / 2 - delegate: StyledRect { - required property string modelData - required property int index + Repeater { + model: Schemes.list - width: ListView.view.width + delegate: StyledRect { + required property var modelData - readonly property bool isCurrent: modelData === rootPane.fontFamilySans - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + Layout.fillWidth: true - StateLayer { - function onClicked(): void { - rootPane.fontFamilySans = modelData; - rootPane.saveConfig(); - } - } + readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` + readonly property bool isCurrent: schemeKey === Schemes.currentScheme - RowLayout { - id: fontFamilySansRow + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + StateLayer { + function onClicked(): void { + const name = modelData.name; + const flavour = modelData.flavour; + const schemeKey = `${name} ${flavour}`; - spacing: Appearance.spacing.normal + // Optimistic update - set immediately for responsive UI + Schemes.currentScheme = schemeKey; + Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + // Reload after a delay to confirm changes + Qt.callLater(() => { + reloadTimer.restart(); + }); + } + } - Item { - Layout.fillWidth: true - } + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); + } + } - Loader { - active: isCurrent - asynchronous: true + RowLayout { + id: schemeRow - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } - } - } + anchors.fill: parent + anchors.margins: Appearance.padding.normal - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 - } - } - } - } + spacing: Appearance.spacing.normal - SectionContainer { - contentSpacing: Appearance.spacing.normal + StyledRect { + id: preview - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + Layout.alignment: Qt.AlignVCenter - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + border.width: 1 + border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) - StyledText { - text: qsTr("Font size scale") - font.pointSize: Appearance.font.size.normal - } + color: `#${modelData.colours?.surface}` + radius: Appearance.rounding.full + implicitWidth: iconPlaceholder.implicitWidth + implicitHeight: iconPlaceholder.implicitWidth - Item { - Layout.fillWidth: true - } + MaterialIcon { + id: iconPlaceholder + visible: false + text: "circle" + font.pointSize: Appearance.font.size.large + } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: fontSizeInput.implicitHeight + Appearance.padding.small * 2 - color: fontSizeInputHover.containsMouse || fontSizeInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: fontSizeInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) + Item { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } + implicitWidth: parent.implicitWidth / 2 + clip: true - MouseArea { - id: fontSizeInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right - StyledTextField { - id: fontSizeInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.7; top: 1.5 } - - Component.onCompleted: { - text = (rootPane.fontSizeScale).toFixed(1); - } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.7 && val <= 1.5) { - rootPane.fontSizeScale = val; - rootPane.saveConfig(); - } + implicitWidth: preview.implicitWidth + color: `#${modelData.colours?.primary}` + radius: Appearance.rounding.full } } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.7 || val > 1.5) { - text = (rootPane.fontSizeScale).toFixed(1); - } + } + + Column { + Layout.fillWidth: true + spacing: 0 + + StyledText { + text: modelData.flavour ?? "" + font.pointSize: Appearance.font.size.normal } - } - } - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } + StyledText { + text: modelData.name ?? "" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline - StyledSlider { - id: fontSizeSlider + elide: Text.ElideRight + anchors.left: parent.left + anchors.right: parent.right + } + } - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + Loader { + active: isCurrent + asynchronous: true - from: 0.7 - to: 1.5 - value: rootPane.fontSizeScale - onMoved: { - rootPane.fontSizeScale = fontSizeSlider.value; - if (!fontSizeInput.activeFocus) { - fontSizeInput.text = (fontSizeSlider.value).toFixed(1); + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } - rootPane.saveConfig(); } + + implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 } } } } CollapsibleSection { - id: scalesSection - title: qsTr("Scales") + id: animationsSection + title: qsTr("Animations") showBackground: true SectionContainer { @@ -881,7 +1005,7 @@ RowLayout { spacing: Appearance.spacing.normal StyledText { - text: qsTr("Padding scale") + text: qsTr("Animation duration scale") font.pointSize: Appearance.font.size.normal } @@ -891,13 +1015,13 @@ RowLayout { StyledRect { Layout.preferredWidth: 70 - implicitHeight: paddingInput.implicitHeight + Appearance.padding.small * 2 - color: paddingInputHover.containsMouse || paddingInput.activeFocus + implicitHeight: animDurationsInput.implicitHeight + Appearance.padding.small * 2 + color: animDurationsInputHover.containsMouse || animDurationsInput.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 - border.color: paddingInput.activeFocus + border.color: animDurationsInput.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -905,7 +1029,7 @@ RowLayout { Behavior on border.color { CAnim {} } MouseArea { - id: paddingInputHover + id: animDurationsInputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -913,29 +1037,29 @@ RowLayout { } StyledTextField { - id: paddingInput + id: animDurationsInput anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.5; top: 2.0 } + validator: DoubleValidator { bottom: 0.1; top: 5.0 } Component.onCompleted: { - text = (rootPane.paddingScale).toFixed(1); + text = (rootPane.animDurationsScale).toFixed(1); } onTextChanged: { if (activeFocus) { const val = parseFloat(text); - if (!isNaN(val) && val >= 0.5 && val <= 2.0) { - rootPane.paddingScale = val; + if (!isNaN(val) && val >= 0.1 && val <= 5.0) { + rootPane.animDurationsScale = val; rootPane.saveConfig(); } } } onEditingFinished: { const val = parseFloat(text); - if (isNaN(val) || val < 0.5 || val > 2.0) { - text = (rootPane.paddingScale).toFixed(1); + if (isNaN(val) || val < 0.1 || val > 5.0) { + text = (rootPane.animDurationsScale).toFixed(1); } } } @@ -949,332 +1073,271 @@ RowLayout { } StyledSlider { - id: paddingSlider + id: animDurationsSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 - from: 0.5 - to: 2.0 - value: rootPane.paddingScale + from: 0.1 + to: 5.0 + value: rootPane.animDurationsScale onMoved: { - rootPane.paddingScale = paddingSlider.value; - if (!paddingInput.activeFocus) { - paddingInput.text = (paddingSlider.value).toFixed(1); + rootPane.animDurationsScale = animDurationsSlider.value; + if (!animDurationsInput.activeFocus) { + animDurationsInput.text = (animDurationsSlider.value).toFixed(1); } rootPane.saveConfig(); } } } } + } - SectionContainer { - contentSpacing: Appearance.spacing.normal + CollapsibleSection { + id: fontsSection + title: qsTr("Fonts") + showBackground: true - ColumnLayout { + CollapsibleSection { + id: materialFontSection + title: qsTr("Material font family") + expanded: true + showBackground: true + nested: true + + Loader { + id: materialFontLoader Layout.fillWidth: true - spacing: Appearance.spacing.small + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: materialFontSection.expanded - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + sourceComponent: StyledListView { + id: materialFontList + property alias contentHeight: materialFontList.contentHeight - StyledText { - text: qsTr("Rounding scale") - font.pointSize: Appearance.font.size.normal - } + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() - Item { - Layout.fillWidth: true + StyledScrollBar.vertical: StyledScrollBar { + flickable: materialFontList } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: roundingInput.implicitHeight + Appearance.padding.small * 2 - color: roundingInputHover.containsMouse || roundingInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: roundingInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) + delegate: StyledRect { + required property string modelData + required property int index - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } + width: ListView.view.width - MouseArea { - id: roundingInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMaterial = modelData; + rootPane.saveConfig(); + } } - StyledTextField { - id: roundingInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 5.0 } - - Component.onCompleted: { - text = (rootPane.roundingScale).toFixed(1); + RowLayout { + id: fontFamilyMaterialRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 5.0) { - rootPane.roundingScale = val; - rootPane.saveConfig(); - } - } + + Item { + Layout.fillWidth: true } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 5.0) { - text = (rootPane.roundingScale).toFixed(1); + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large } } } - } - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 } } - StyledSlider { - id: roundingSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0.1 - to: 5.0 - value: rootPane.roundingScale - onMoved: { - rootPane.roundingScale = roundingSlider.value; - if (!roundingInput.activeFocus) { - roundingInput.text = (roundingSlider.value).toFixed(1); - } - rootPane.saveConfig(); - } - } } } - SectionContainer { - contentSpacing: Appearance.spacing.normal + CollapsibleSection { + id: monoFontSection + title: qsTr("Monospace font family") + expanded: false + showBackground: true + nested: true - ColumnLayout { + Loader { Layout.fillWidth: true - spacing: Appearance.spacing.small + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: monoFontSection.expanded - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + sourceComponent: StyledListView { + id: monoFontList + property alias contentHeight: monoFontList.contentHeight - StyledText { - text: qsTr("Spacing scale") - font.pointSize: Appearance.font.size.normal + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() + + StyledScrollBar.vertical: StyledScrollBar { + flickable: monoFontList } - Item { - Layout.fillWidth: true - } + delegate: StyledRect { + required property string modelData + required property int index - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: spacingInput.implicitHeight + Appearance.padding.small * 2 - color: spacingInputHover.containsMouse || spacingInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: spacingInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) + width: ListView.view.width - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - MouseArea { - id: spacingInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMono = modelData; + rootPane.saveConfig(); + } } - StyledTextField { - id: spacingInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 2.0 } - - Component.onCompleted: { - text = (rootPane.spacingScale).toFixed(1); + RowLayout { + id: fontFamilyMonoRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 2.0) { - rootPane.spacingScale = val; - rootPane.saveConfig(); - } - } + + Item { + Layout.fillWidth: true } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 2.0) { - text = (rootPane.spacingScale).toFixed(1); + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large } } } - } - - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } - - StyledSlider { - id: spacingSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - from: 0.1 - to: 2.0 - value: rootPane.spacingScale - onMoved: { - rootPane.spacingScale = spacingSlider.value; - if (!spacingInput.activeFocus) { - spacingInput.text = (spacingSlider.value).toFixed(1); - } - rootPane.saveConfig(); + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 } } } } - } - - CollapsibleSection { - id: transparencySection - title: qsTr("Transparency") - showBackground: true - - SwitchRow { - label: qsTr("Transparency enabled") - checked: rootPane.transparencyEnabled - onToggled: checked => { - rootPane.transparencyEnabled = checked; - rootPane.saveConfig(); - } - } - SectionContainer { - contentSpacing: Appearance.spacing.normal + CollapsibleSection { + id: sansFontSection + title: qsTr("Sans-serif font family") + expanded: false + showBackground: true + nested: true - ColumnLayout { + Loader { Layout.fillWidth: true - spacing: Appearance.spacing.small + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: sansFontSection.expanded - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + sourceComponent: StyledListView { + id: sansFontList + property alias contentHeight: sansFontList.contentHeight - StyledText { - text: qsTr("Transparency base") - font.pointSize: Appearance.font.size.normal - } + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() - Item { - Layout.fillWidth: true + StyledScrollBar.vertical: StyledScrollBar { + flickable: sansFontList } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: transparencyBaseInput.implicitHeight + Appearance.padding.small * 2 - color: transparencyBaseInputHover.containsMouse || transparencyBaseInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: transparencyBaseInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) + delegate: StyledRect { + required property string modelData + required property int index - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } + width: ListView.view.width - MouseArea { - id: transparencyBaseInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } + readonly property bool isCurrent: modelData === rootPane.fontFamilySans + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StyledTextField { - id: transparencyBaseInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } - - Component.onCompleted: { - text = Math.round(rootPane.transparencyBase * 100).toString(); - } - - onTextChanged: { - if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - rootPane.transparencyBase = val / 100; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(rootPane.transparencyBase * 100).toString(); - } + StateLayer { + function onClicked(): void { + rootPane.fontFamilySans = modelData; + rootPane.saveConfig(); } } - } - StyledText { - text: "%" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } + RowLayout { + id: fontFamilySansRow - StyledSlider { - id: baseSlider + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + spacing: Appearance.spacing.normal - from: 0 - to: 100 - value: rootPane.transparencyBase * 100 - onMoved: { - rootPane.transparencyBase = baseSlider.value / 100; - if (!transparencyBaseInput.activeFocus) { - transparencyBaseInput.text = Math.round(baseSlider.value).toString(); + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } } - rootPane.saveConfig(); + + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 } } } @@ -1292,7 +1355,7 @@ RowLayout { spacing: Appearance.spacing.normal StyledText { - text: qsTr("Transparency layers") + text: qsTr("Font size scale") font.pointSize: Appearance.font.size.normal } @@ -1302,13 +1365,13 @@ RowLayout { StyledRect { Layout.preferredWidth: 70 - implicitHeight: transparencyLayersInput.implicitHeight + Appearance.padding.small * 2 - color: transparencyLayersInputHover.containsMouse || transparencyLayersInput.activeFocus + implicitHeight: fontSizeInput.implicitHeight + Appearance.padding.small * 2 + color: fontSizeInputHover.containsMouse || fontSizeInput.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 - border.color: transparencyLayersInput.activeFocus + border.color: fontSizeInput.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -1316,7 +1379,7 @@ RowLayout { Behavior on border.color { CAnim {} } MouseArea { - id: transparencyLayersInputHover + id: fontSizeInputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -1324,56 +1387,56 @@ RowLayout { } StyledTextField { - id: transparencyLayersInput + id: fontSizeInput anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } + validator: DoubleValidator { bottom: 0.7; top: 1.5 } Component.onCompleted: { - text = Math.round(rootPane.transparencyLayers * 100).toString(); + text = (rootPane.fontSizeScale).toFixed(1); } onTextChanged: { if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - rootPane.transparencyLayers = val / 100; + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.7 && val <= 1.5) { + rootPane.fontSizeScale = val; rootPane.saveConfig(); } } } onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(rootPane.transparencyLayers * 100).toString(); + const val = parseFloat(text); + if (isNaN(val) || val < 0.7 || val > 1.5) { + text = (rootPane.fontSizeScale).toFixed(1); } } } } StyledText { - text: "%" + text: "×" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal } } StyledSlider { - id: layersSlider + id: fontSizeSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 - from: 0 - to: 100 - value: rootPane.transparencyLayers * 100 + from: 0.7 + to: 1.5 + value: rootPane.fontSizeScale onMoved: { - rootPane.transparencyLayers = layersSlider.value / 100; - if (!transparencyLayersInput.activeFocus) { - transparencyLayersInput.text = Math.round(layersSlider.value).toString(); + rootPane.fontSizeScale = fontSizeSlider.value; + if (!fontSizeInput.activeFocus) { + fontSizeInput.text = (fontSizeSlider.value).toFixed(1); } - rootPane.saveConfig(); + rootPane.saveConfig(); } } } @@ -1381,8 +1444,8 @@ RowLayout { } CollapsibleSection { - id: borderSection - title: qsTr("Border") + id: scalesSection + title: qsTr("Scales") showBackground: true SectionContainer { @@ -1397,7 +1460,7 @@ RowLayout { spacing: Appearance.spacing.normal StyledText { - text: qsTr("Border rounding") + text: qsTr("Padding scale") font.pointSize: Appearance.font.size.normal } @@ -1407,13 +1470,13 @@ RowLayout { StyledRect { Layout.preferredWidth: 70 - implicitHeight: borderRoundingInput.implicitHeight + Appearance.padding.small * 2 - color: borderRoundingInputHover.containsMouse || borderRoundingInput.activeFocus + implicitHeight: paddingInput.implicitHeight + Appearance.padding.small * 2 + color: paddingInputHover.containsMouse || paddingInput.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 - border.color: borderRoundingInput.activeFocus + border.color: paddingInput.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -1421,7 +1484,7 @@ RowLayout { Behavior on border.color { CAnim {} } MouseArea { - id: borderRoundingInputHover + id: paddingInputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -1429,48 +1492,54 @@ RowLayout { } StyledTextField { - id: borderRoundingInput + id: paddingInput anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 100 } + validator: DoubleValidator { bottom: 0.5; top: 2.0 } Component.onCompleted: { - text = (rootPane.borderRounding).toFixed(1); + text = (rootPane.paddingScale).toFixed(1); } onTextChanged: { if (activeFocus) { const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 100) { - rootPane.borderRounding = val; + if (!isNaN(val) && val >= 0.5 && val <= 2.0) { + rootPane.paddingScale = val; rootPane.saveConfig(); } } } onEditingFinished: { const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 100) { - text = (rootPane.borderRounding).toFixed(1); + if (isNaN(val) || val < 0.5 || val > 2.0) { + text = (rootPane.paddingScale).toFixed(1); } } } } + + StyledText { + text: "×" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } } StyledSlider { - id: borderRoundingSlider + id: paddingSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 - from: 0.1 - to: 100 - value: rootPane.borderRounding + from: 0.5 + to: 2.0 + value: rootPane.paddingScale onMoved: { - rootPane.borderRounding = borderRoundingSlider.value; - if (!borderRoundingInput.activeFocus) { - borderRoundingInput.text = (borderRoundingSlider.value).toFixed(1); + rootPane.paddingScale = paddingSlider.value; + if (!paddingInput.activeFocus) { + paddingInput.text = (paddingSlider.value).toFixed(1); } rootPane.saveConfig(); } @@ -1490,7 +1559,7 @@ RowLayout { spacing: Appearance.spacing.normal StyledText { - text: qsTr("Border thickness") + text: qsTr("Rounding scale") font.pointSize: Appearance.font.size.normal } @@ -1500,13 +1569,13 @@ RowLayout { StyledRect { Layout.preferredWidth: 70 - implicitHeight: borderThicknessInput.implicitHeight + Appearance.padding.small * 2 - color: borderThicknessInputHover.containsMouse || borderThicknessInput.activeFocus + implicitHeight: roundingInput.implicitHeight + Appearance.padding.small * 2 + color: roundingInputHover.containsMouse || roundingInput.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 - border.color: borderThicknessInput.activeFocus + border.color: roundingInput.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -1514,7 +1583,7 @@ RowLayout { Behavior on border.color { CAnim {} } MouseArea { - id: borderThicknessInputHover + id: roundingInputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -1522,103 +1591,60 @@ RowLayout { } StyledTextField { - id: borderThicknessInput + id: roundingInput anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 100 } + validator: DoubleValidator { bottom: 0.1; top: 5.0 } Component.onCompleted: { - text = (rootPane.borderThickness).toFixed(1); + text = (rootPane.roundingScale).toFixed(1); } onTextChanged: { if (activeFocus) { const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 100) { - rootPane.borderThickness = val; + if (!isNaN(val) && val >= 0.1 && val <= 5.0) { + rootPane.roundingScale = val; rootPane.saveConfig(); } } } onEditingFinished: { const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 100) { - text = (rootPane.borderThickness).toFixed(1); + if (isNaN(val) || val < 0.1 || val > 5.0) { + text = (rootPane.roundingScale).toFixed(1); } } } } + + StyledText { + text: "×" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } } StyledSlider { - id: borderThicknessSlider + id: roundingSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 from: 0.1 - to: 100 - value: rootPane.borderThickness + to: 5.0 + value: rootPane.roundingScale onMoved: { - rootPane.borderThickness = borderThicknessSlider.value; - if (!borderThicknessInput.activeFocus) { - borderThicknessInput.text = (borderThicknessSlider.value).toFixed(1); + rootPane.roundingScale = roundingSlider.value; + if (!roundingInput.activeFocus) { + roundingInput.text = (roundingSlider.value).toFixed(1); } rootPane.saveConfig(); } } } } - } - - CollapsibleSection { - id: backgroundSection - title: qsTr("Background") - showBackground: true - - SwitchRow { - label: qsTr("Desktop clock") - checked: rootPane.desktopClockEnabled - onToggled: checked => { - rootPane.desktopClockEnabled = checked; - rootPane.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Background enabled") - checked: rootPane.backgroundEnabled - onToggled: checked => { - rootPane.backgroundEnabled = checked; - rootPane.saveConfig(); - } - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Visualiser") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - SwitchRow { - label: qsTr("Visualiser enabled") - checked: rootPane.visualiserEnabled - onToggled: checked => { - rootPane.visualiserEnabled = checked; - rootPane.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Visualiser auto hide") - checked: rootPane.visualiserAutoHide - onToggled: checked => { - rootPane.visualiserAutoHide = checked; - rootPane.saveConfig(); - } - } SectionContainer { contentSpacing: Appearance.spacing.normal @@ -1632,7 +1658,7 @@ RowLayout { spacing: Appearance.spacing.normal StyledText { - text: qsTr("Visualiser rounding") + text: qsTr("Spacing scale") font.pointSize: Appearance.font.size.normal } @@ -1642,13 +1668,13 @@ RowLayout { StyledRect { Layout.preferredWidth: 70 - implicitHeight: visualiserRoundingInput.implicitHeight + Appearance.padding.small * 2 - color: visualiserRoundingInputHover.containsMouse || visualiserRoundingInput.activeFocus + implicitHeight: spacingInput.implicitHeight + Appearance.padding.small * 2 + color: spacingInputHover.containsMouse || spacingInput.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 - border.color: visualiserRoundingInput.activeFocus + border.color: spacingInput.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -1656,7 +1682,7 @@ RowLayout { Behavior on border.color { CAnim {} } MouseArea { - id: visualiserRoundingInputHover + id: spacingInputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -1664,55 +1690,75 @@ RowLayout { } StyledTextField { - id: visualiserRoundingInput + id: spacingInput anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 10 } + validator: DoubleValidator { bottom: 0.1; top: 2.0 } Component.onCompleted: { - text = Math.round(rootPane.visualiserRounding).toString(); + text = (rootPane.spacingScale).toFixed(1); } onTextChanged: { if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 10) { - rootPane.visualiserRounding = val; - rootPane.saveConfig(); + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 2.0) { + rootPane.spacingScale = val; + rootPane.saveConfig(); } } } onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 10) { - text = Math.round(rootPane.visualiserRounding).toString(); + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 2.0) { + text = (rootPane.spacingScale).toFixed(1); } } } } + + StyledText { + text: "×" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } } StyledSlider { - id: visualiserRoundingSlider + id: spacingSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 - from: 0 - to: 10 - stepSize: 1 - value: rootPane.visualiserRounding + from: 0.1 + to: 2.0 + value: rootPane.spacingScale onMoved: { - rootPane.visualiserRounding = Math.round(visualiserRoundingSlider.value); - if (!visualiserRoundingInput.activeFocus) { - visualiserRoundingInput.text = Math.round(visualiserRoundingSlider.value).toString(); + rootPane.spacingScale = spacingSlider.value; + if (!spacingInput.activeFocus) { + spacingInput.text = (spacingSlider.value).toFixed(1); } rootPane.saveConfig(); + } + } } } } - } + + CollapsibleSection { + id: transparencySection + title: qsTr("Transparency") + showBackground: true + + SwitchRow { + label: qsTr("Transparency enabled") + checked: rootPane.transparencyEnabled + onToggled: checked => { + rootPane.transparencyEnabled = checked; + rootPane.saveConfig(); + } + } SectionContainer { contentSpacing: Appearance.spacing.normal @@ -1726,7 +1772,7 @@ RowLayout { spacing: Appearance.spacing.normal StyledText { - text: qsTr("Visualiser spacing") + text: qsTr("Transparency base") font.pointSize: Appearance.font.size.normal } @@ -1736,13 +1782,13 @@ RowLayout { StyledRect { Layout.preferredWidth: 70 - implicitHeight: visualiserSpacingInput.implicitHeight + Appearance.padding.small * 2 - color: visualiserSpacingInputHover.containsMouse || visualiserSpacingInput.activeFocus + implicitHeight: transparencyBaseInput.implicitHeight + Appearance.padding.small * 2 + color: transparencyBaseInputHover.containsMouse || transparencyBaseInput.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 - border.color: visualiserSpacingInput.activeFocus + border.color: transparencyBaseInput.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -1750,7 +1796,7 @@ RowLayout { Behavior on border.color { CAnim {} } MouseArea { - id: visualiserSpacingInputHover + id: transparencyBaseInputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -1758,701 +1804,592 @@ RowLayout { } StyledTextField { - id: visualiserSpacingInput + id: transparencyBaseInput anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0; top: 2 } + validator: IntValidator { bottom: 0; top: 100 } Component.onCompleted: { - text = (rootPane.visualiserSpacing).toFixed(1); + text = Math.round(rootPane.transparencyBase * 100).toString(); } onTextChanged: { if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0 && val <= 2) { - rootPane.visualiserSpacing = val; + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + rootPane.transparencyBase = val / 100; rootPane.saveConfig(); } } } onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0 || val > 2) { - text = (rootPane.visualiserSpacing).toFixed(1); + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(rootPane.transparencyBase * 100).toString(); } } } } + + StyledText { + text: "%" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } } StyledSlider { - id: visualiserSpacingSlider + id: baseSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 from: 0 - to: 2 - value: rootPane.visualiserSpacing + to: 100 + value: rootPane.transparencyBase * 100 onMoved: { - rootPane.visualiserSpacing = visualiserSpacingSlider.value; - if (!visualiserSpacingInput.activeFocus) { - visualiserSpacingInput.text = (visualiserSpacingSlider.value).toFixed(1); + rootPane.transparencyBase = baseSlider.value / 100; + if (!transparencyBaseInput.activeFocus) { + transparencyBaseInput.text = Math.round(baseSlider.value).toString(); } rootPane.saveConfig(); } } } } - } - } - } - } - } - - 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 + SectionContainer { + contentSpacing: Appearance.spacing.normal - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: Appearance.spacing.normal + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - MaterialIcon { - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.topMargin: 0 - text: "palette" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Appearance Settings") - font.pointSize: Appearance.font.size.large - font.bold: true - } + StyledText { + text: qsTr("Transparency layers") + font.pointSize: Appearance.font.size.normal + } - StyledText { - Layout.topMargin: Appearance.spacing.large - Layout.alignment: Qt.AlignHCenter - text: qsTr("Wallpaper") - font.pointSize: Appearance.font.size.extraLarge - font.weight: 600 - } + Item { + Layout.fillWidth: true + } - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Select a wallpaper") - font.pointSize: Appearance.font.size.normal - color: Colours.palette.m3onSurfaceVariant - } + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: transparencyLayersInput.implicitHeight + Appearance.padding.small * 2 + color: transparencyLayersInputHover.containsMouse || transparencyLayersInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: transparencyLayersInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) - 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; - } - } - } + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } - 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 + MouseArea { + id: transparencyLayersInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } - // 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; + StyledTextField { + id: transparencyLayersInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 100 } - // 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]}); - } + Component.onCompleted: { + text = Math.round(rootPane.transparencyLayers * 100).toString(); } - }); - } - - 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; + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + rootPane.transparencyLayers = val / 100; + rootPane.saveConfig(); + } } - 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]}); + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(rootPane.transparencyLayers * 100).toString(); } } } } - - 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]}); - } + + StyledText { + text: "%" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: layersSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0 + to: 100 + value: rootPane.transparencyLayers * 100 + onMoved: { + rootPane.transparencyLayers = layersSlider.value / 100; + if (!transparencyLayersInput.activeFocus) { + transparencyLayersInput.text = Math.round(layersSlider.value).toString(); } + rootPane.saveConfig(); } - - // 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; + } + } + } + } + + CollapsibleSection { + id: borderSection + title: qsTr("Border") + showBackground: true + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Border rounding") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: borderRoundingInput.implicitHeight + Appearance.padding.small * 2 + color: borderRoundingInputHover.containsMouse || borderRoundingInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: borderRoundingInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: borderRoundingInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: borderRoundingInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0.1; top: 100 } - if (!lazyModel || !lazyModel.sourceList || lazyModel.loadedCount >= lazyModel.totalCount || wallpaperGrid._expansionInProgress) { - return; + Component.onCompleted: { + text = (rootPane.borderRounding).toFixed(1); } - 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; - }); + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 100) { + rootPane.borderRounding = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 100) { + text = (rootPane.borderRounding).toFixed(1); } } } } - - // 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; + } + + StyledSlider { + id: borderRoundingSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0.1 + to: 100 + value: rootPane.borderRounding + onMoved: { + rootPane.borderRounding = borderRoundingSlider.value; + if (!borderRoundingInput.activeFocus) { + borderRoundingInput.text = (borderRoundingSlider.value).toFixed(1); + } + rootPane.saveConfig(); + } + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Border thickness") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: borderThicknessInput.implicitHeight + Appearance.padding.small * 2 + color: borderThicknessInputHover.containsMouse || borderThicknessInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: borderThicknessInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: borderThicknessInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton } - 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); + + StyledTextField { + id: borderThicknessInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0.1; top: 100 } - // 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); + Component.onCompleted: { + text = (rootPane.borderThickness).toFixed(1); + } - if (rowsRemaining <= 1 && lazyModel.loadedCount < lazyModel.totalCount) { - if (!wallpaperGrid._expansionInProgress) { - wallpaperGrid._expansionInProgress = true; - lazyModel.loadOneRow(); - Qt.callLater(function() { - wallpaperGrid._expansionInProgress = false; - }); + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 100) { + rootPane.borderThickness = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 100) { + text = (rootPane.borderThickness).toFixed(1); } } } } - - - // Parent Flickable handles scrolling - interactive: false + } + StyledSlider { + id: borderThicknessSlider - delegate: Item { - required property var modelData + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 - width: wallpaperGrid.cellWidth - height: wallpaperGrid.cellHeight + from: 0.1 + to: 100 + value: rootPane.borderThickness + onMoved: { + rootPane.borderThickness = borderThicknessSlider.value; + if (!borderThicknessInput.activeFocus) { + borderThicknessInput.text = (borderThicknessSlider.value).toFixed(1); + } + rootPane.saveConfig(); + } + } + } + } + } - 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++; + CollapsibleSection { + id: backgroundSection + title: qsTr("Background") + showBackground: true + + SwitchRow { + label: qsTr("Desktop clock") + checked: rootPane.desktopClockEnabled + onToggled: checked => { + rootPane.desktopClockEnabled = checked; + rootPane.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Background enabled") + checked: rootPane.backgroundEnabled + onToggled: checked => { + rootPane.backgroundEnabled = checked; + rootPane.saveConfig(); } + } - StateLayer { - anchors.fill: parent - anchors.leftMargin: itemMargin - anchors.rightMargin: itemMargin - anchors.topMargin: itemMargin - anchors.bottomMargin: itemMargin - radius: itemRadius + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Visualiser") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } - function onClicked(): void { - Wallpapers.setWallpaper(modelData.path); - } + SwitchRow { + label: qsTr("Visualiser enabled") + checked: rootPane.visualiserEnabled + onToggled: checked => { + rootPane.visualiserEnabled = checked; + rootPane.saveConfig(); } + } - StyledClippingRect { - id: image + SwitchRow { + label: qsTr("Visualiser auto hide") + checked: rootPane.visualiserAutoHide + onToggled: checked => { + rootPane.visualiserAutoHide = checked; + rootPane.saveConfig(); + } + } - 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 + SectionContainer { + contentSpacing: Appearance.spacing.normal - CachingImage { - id: cachingImage + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - path: modelData.path - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - cache: true - visible: opacity > 0 - antialiasing: true - smooth: true + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - opacity: status === Image.Ready ? 1 : 0 + StyledText { + text: qsTr("Visualiser rounding") + font.pointSize: Appearance.font.size.normal + } - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutQuad - } + Item { + Layout.fillWidth: true } - } - // Fallback if CachingImage fails to load - Image { - id: fallbackImage + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: visualiserRoundingInput.implicitHeight + Appearance.padding.small * 2 + color: visualiserRoundingInputHover.containsMouse || visualiserRoundingInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: visualiserRoundingInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) - 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 + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } - opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 + MouseArea { + id: visualiserRoundingInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutQuad + StyledTextField { + id: visualiserRoundingInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 10 } + + Component.onCompleted: { + text = Math.round(rootPane.visualiserRounding).toString(); + } + + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 10) { + rootPane.visualiserRounding = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 10) { + text = Math.round(rootPane.visualiserRounding).toString(); + } + } } } } - 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 + StyledSlider { + id: visualiserRoundingSlider - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 - 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) + from: 0 + to: 10 + stepSize: 1 + value: rootPane.visualiserRounding + onMoved: { + rootPane.visualiserRounding = Math.round(visualiserRoundingSlider.value); + if (!visualiserRoundingInput.activeFocus) { + visualiserRoundingInput.text = Math.round(visualiserRoundingSlider.value).toString(); } - } + rootPane.saveConfig(); + } + } + } + } - opacity: 0 + SectionContainer { + contentSpacing: Appearance.spacing.normal - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic - } - } + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - Component.onCompleted: { - opacity = 1; - } - } - } + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - 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 + StyledText { + text: qsTr("Visualiser spacing") + font.pointSize: Appearance.font.size.normal + } - Behavior on border.width { - NumberAnimation { - duration: 150 - easing.type: Easing.OutQuad + Item { + Layout.fillWidth: true } - } - MaterialIcon { - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Appearance.padding.small + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: visualiserSpacingInput.implicitHeight + Appearance.padding.small * 2 + color: visualiserSpacingInputHover.containsMouse || visualiserSpacingInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: visualiserSpacingInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) - visible: isCurrent - text: "check_circle" - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large - } - } + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } - 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 + MouseArea { + id: visualiserSpacingInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } - readonly property string fileName: { - const path = modelData.relativePath || ""; - const parts = path.split("/"); - return parts.length > 0 ? parts[parts.length - 1] : path; + StyledTextField { + id: visualiserSpacingInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0; top: 2 } + + Component.onCompleted: { + text = (rootPane.visualiserSpacing).toFixed(1); + } + + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0 && val <= 2) { + rootPane.visualiserSpacing = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0 || val > 2) { + text = (rootPane.visualiserSpacing).toFixed(1); + } + } + } + } } - 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 + StyledSlider { + id: visualiserSpacingSlider - opacity: 0 + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic + from: 0 + to: 2 + value: rootPane.visualiserSpacing + onMoved: { + rootPane.visualiserSpacing = visualiserSpacingSlider.value; + if (!visualiserSpacingInput.activeFocus) { + visualiserSpacingInput.text = (visualiserSpacingSlider.value).toFixed(1); + } + rootPane.saveConfig(); } } - - 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 - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: outputVolumeInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } + 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 {} } + + 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 - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: inputVolumeInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } + 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 {} } + + 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 + + anchors.centerIn: parent + text: "mic_off" + color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer } } } - MaterialIcon { - id: muteInputIcon + StyledSlider { + id: inputVolumeSlider + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 - 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 - - 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 - - radius: rightBorder.innerRadius - color: "transparent" - + rightContent: Component { + Item { + id: rightPaneItem + // Right pane - networking details/settings - Loader { - id: loader + 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 + + function getComponentForPane() { + return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : 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 + Component.onCompleted: { + targetComponent = getComponentForPane(); + nextComponent = targetComponent; + } - function getComponentForPane() { - return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings; + Connections { + target: root.session.ethernet + function onActiveChanged() { + nextComponent = getComponentForPane(); + paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + } + } + + 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 } -- cgit v1.2.3-freya