summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/controlcenter/appearance/AppearancePane.qml1291
-rw-r--r--modules/controlcenter/audio/AudioPane.qml562
-rw-r--r--modules/controlcenter/bluetooth/BtPane.qml138
-rw-r--r--modules/controlcenter/components/SplitPaneLayout.qml120
-rw-r--r--modules/controlcenter/launcher/LauncherPane.qml288
-rw-r--r--modules/controlcenter/network/NetworkingPane.qml294
6 files changed, 1287 insertions, 1406 deletions
diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml
index 61cdcaa..2041bf8 100644
--- a/modules/controlcenter/appearance/AppearancePane.qml
+++ b/modules/controlcenter/appearance/AppearancePane.qml
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import ".."
+import "../components"
import "../../launcher/services"
import qs.components
import qs.components.controls
@@ -16,7 +17,7 @@ import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
-RowLayout {
+Item {
id: root
required property Session session
@@ -46,9 +47,6 @@ RowLayout {
anchors.fill: parent
- spacing: 0
-
-
function saveConfig() {
Config.appearance.anim.durations.scale = root.animDurationsScale;
@@ -79,45 +77,626 @@ RowLayout {
Config.save();
}
- Item {
- id: leftAppearanceItem
- Layout.preferredWidth: Math.floor(parent.width * 0.4)
- Layout.minimumWidth: 420
- Layout.fillHeight: true
+ Component {
+ id: appearanceRightContentComponent
- ClippingRectangle {
- id: leftAppearanceClippingRect
- anchors.fill: parent
- anchors.margins: Appearance.padding.normal
- anchors.leftMargin: 0
- anchors.rightMargin: Appearance.padding.normal / 2
- radius: leftAppearanceBorder.innerRadius
- color: "transparent"
+ StyledFlickable {
+ id: rightAppearanceFlickable
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: contentLayout.height
- Loader {
- id: leftAppearanceLoader
- anchors.fill: parent
- anchors.margins: Appearance.padding.large + Appearance.padding.normal
- anchors.leftMargin: Appearance.padding.large
- anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
- asynchronous: true
- sourceComponent: appearanceLeftContentComponent
- property var rootPane: root
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: rightAppearanceFlickable
}
- }
- InnerBorder {
- id: leftAppearanceBorder
- leftThickness: 0
- rightThickness: Appearance.padding.normal / 2
+ ColumnLayout {
+ id: contentLayout
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ spacing: Appearance.spacing.normal
+
+ MaterialIcon {
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
+ Layout.topMargin: 0
+ text: "palette"
+ font.pointSize: Appearance.font.size.extraLarge * 3
+ font.bold: true
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ text: qsTr("Appearance Settings")
+ font.pointSize: Appearance.font.size.large
+ font.bold: true
+ }
+
+ StyledText {
+ Layout.topMargin: Appearance.spacing.large
+ Layout.alignment: Qt.AlignHCenter
+ text: qsTr("Wallpaper")
+ font.pointSize: Appearance.font.size.extraLarge
+ font.weight: 600
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ text: qsTr("Select a wallpaper")
+ font.pointSize: Appearance.font.size.normal
+ color: Colours.palette.m3onSurfaceVariant
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.topMargin: Appearance.spacing.large
+ Layout.preferredHeight: wallpaperLoader.item ? wallpaperLoader.item.layoutPreferredHeight : 0
+
+ Loader {
+ id: wallpaperLoader
+ anchors.fill: parent
+ asynchronous: true
+ active: {
+ // Lazy load: only activate when:
+ // 1. Right pane is loaded AND
+ // 2. Appearance pane is active (index 3) or adjacent (for smooth transitions)
+ // This prevents loading all wallpapers when control center opens but appearance pane isn't visible
+ const isActive = root.session.activeIndex === 3;
+ const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1;
+ // Access loader through SplitPaneLayout's rightLoader
+ const splitLayout = root.children[0];
+ const loader = splitLayout && splitLayout.rightLoader ? splitLayout.rightLoader : null;
+ const shouldActivate = loader && loader.item !== null && (isActive || isAdjacent);
+ return shouldActivate;
+ }
+
+ onStatusChanged: {
+ if (status === Loader.Error) {
+ console.error("[AppearancePane] Wallpaper loader error!");
+ }
+ }
+
+ // Stop lazy loading when loader becomes inactive
+ onActiveChanged: {
+ if (!active && wallpaperLoader.item) {
+ const container = wallpaperLoader.item;
+ // Access timer through wallpaperGrid
+ if (container && container.wallpaperGrid) {
+ const grid = container.wallpaperGrid;
+ if (grid.imageUpdateTimer) {
+ grid.imageUpdateTimer.stop();
+ }
+ }
+ }
+ }
+
+ sourceComponent: Item {
+ id: wallpaperGridContainer
+ property alias layoutPreferredHeight: wallpaperGrid.layoutPreferredHeight
+
+ // Find and store reference to parent Flickable for scroll monitoring
+ property var parentFlickable: {
+ let item = parent;
+ while (item) {
+ if (item.flickableDirection !== undefined) {
+ return item;
+ }
+ item = item.parent;
+ }
+ return null;
+ }
+
+ // Cleanup when component is destroyed
+ Component.onDestruction: {
+ if (wallpaperGrid) {
+ if (wallpaperGrid.scrollCheckTimer) {
+ wallpaperGrid.scrollCheckTimer.stop();
+ }
+ wallpaperGrid._expansionInProgress = false;
+ }
+ }
+
+ // Lazy loading model: loads one image at a time, only when touching bottom
+ // This prevents GridView from creating all delegates at once
+ QtObject {
+ id: lazyModel
+
+ property var sourceList: null
+ property int loadedCount: 0 // Total items available to load
+ property int visibleCount: 0 // Items actually exposed to GridView (only visible + buffer)
+ property int totalCount: 0
+
+ function initialize(list) {
+ sourceList = list;
+ totalCount = list ? list.length : 0;
+ // Start with enough items to fill the initial viewport (~3 rows)
+ const initialRows = 3;
+ const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 3;
+ const initialCount = Math.min(initialRows * cols, totalCount);
+ loadedCount = initialCount;
+ visibleCount = initialCount;
+ }
+
+ function loadOneRow() {
+ if (loadedCount < totalCount) {
+ const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 1;
+ const itemsToLoad = Math.min(cols, totalCount - loadedCount);
+ loadedCount += itemsToLoad;
+ }
+ }
+
+ function updateVisibleCount(neededCount) {
+ // Always round up to complete rows to avoid incomplete rows in the grid
+ const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 1;
+ const maxVisible = Math.min(neededCount, loadedCount);
+ const rows = Math.ceil(maxVisible / cols);
+ const newVisibleCount = Math.min(rows * cols, loadedCount);
+
+ if (newVisibleCount > visibleCount) {
+ visibleCount = newVisibleCount;
+ }
+ }
+ }
+
+ GridView {
+ id: wallpaperGrid
+ anchors.fill: parent
+
+ property int _delegateCount: 0
+
+ readonly property int minCellWidth: 200 + Appearance.spacing.normal
+ readonly property int columnsCount: Math.max(1, Math.floor(parent.width / minCellWidth))
+
+ // Height based on visible items only - prevents GridView from creating all delegates
+ readonly property int layoutPreferredHeight: {
+ if (!lazyModel || lazyModel.visibleCount === 0 || columnsCount === 0) {
+ return 0;
+ }
+ const calculated = Math.ceil(lazyModel.visibleCount / columnsCount) * cellHeight;
+ return calculated;
+ }
+
+ height: layoutPreferredHeight
+ cellWidth: width / columnsCount
+ cellHeight: 140 + Appearance.spacing.normal
+
+ leftMargin: 0
+ rightMargin: 0
+ topMargin: 0
+ bottomMargin: 0
+
+ // Use ListModel for incremental updates to prevent flashing when new items are added
+ ListModel {
+ id: wallpaperListModel
+ }
+
+ model: wallpaperListModel
+
+ Connections {
+ target: lazyModel
+ function onVisibleCountChanged(): void {
+ if (!lazyModel || !lazyModel.sourceList) return;
+
+ const newCount = lazyModel.visibleCount;
+ const currentCount = wallpaperListModel.count;
+
+ // Only append new items - never remove or replace existing ones
+ if (newCount > currentCount) {
+ const flickable = wallpaperGridContainer.parentFlickable;
+ const oldScrollY = flickable ? flickable.contentY : 0;
+
+ for (let i = currentCount; i < newCount; i++) {
+ wallpaperListModel.append({modelData: lazyModel.sourceList[i]});
+ }
+
+ // Preserve scroll position after model update
+ if (flickable) {
+ Qt.callLater(function() {
+ if (Math.abs(flickable.contentY - oldScrollY) < 1) {
+ flickable.contentY = oldScrollY;
+ }
+ });
+ }
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ Qt.callLater(function() {
+ const isActive = root.session.activeIndex === 3;
+ if (width > 0 && parent && parent.visible && isActive && Wallpapers.list) {
+ lazyModel.initialize(Wallpapers.list);
+ wallpaperListModel.clear();
+ for (let i = 0; i < lazyModel.visibleCount; i++) {
+ wallpaperListModel.append({modelData: lazyModel.sourceList[i]});
+ }
+ }
+ });
+ }
+
+ Connections {
+ target: root.session
+ function onActiveIndexChanged(): void {
+ const isActive = root.session.activeIndex === 3;
+
+ // Stop lazy loading when switching away from appearance pane
+ if (!isActive) {
+ if (scrollCheckTimer) {
+ scrollCheckTimer.stop();
+ }
+ if (wallpaperGrid) {
+ wallpaperGrid._expansionInProgress = false;
+ }
+ return;
+ }
+
+ // Initialize if needed when switching to appearance pane
+ if (isActive && width > 0 && !lazyModel.sourceList && parent && parent.visible && Wallpapers.list) {
+ lazyModel.initialize(Wallpapers.list);
+ wallpaperListModel.clear();
+ for (let i = 0; i < lazyModel.visibleCount; i++) {
+ wallpaperListModel.append({modelData: lazyModel.sourceList[i]});
+ }
+ }
+ }
+ }
+
+ onWidthChanged: {
+ const isActive = root.session.activeIndex === 3;
+ if (width > 0 && !lazyModel.sourceList && parent && parent.visible && isActive && Wallpapers.list) {
+ lazyModel.initialize(Wallpapers.list);
+ wallpaperListModel.clear();
+ for (let i = 0; i < lazyModel.visibleCount; i++) {
+ wallpaperListModel.append({modelData: lazyModel.sourceList[i]});
+ }
+ }
+ }
+
+ // Force true lazy loading: only create delegates for visible items
+ displayMarginBeginning: 0
+ displayMarginEnd: 0
+ cacheBuffer: 0
+
+ // Debounce expansion to avoid too frequent checks
+ property bool _expansionInProgress: false
+
+ Connections {
+ target: wallpaperGridContainer.parentFlickable
+ function onContentYChanged(): void {
+ // Don't process scroll events if appearance pane is not active
+ const isActive = root.session.activeIndex === 3;
+ if (!isActive) return;
+
+ if (!lazyModel || !lazyModel.sourceList || lazyModel.loadedCount >= lazyModel.totalCount || wallpaperGrid._expansionInProgress) {
+ return;
+ }
+
+ const flickable = wallpaperGridContainer.parentFlickable;
+ if (!flickable) return;
+
+ const gridY = wallpaperGridContainer.y;
+ const scrollY = flickable.contentY;
+ const viewportHeight = flickable.height;
+
+ const topY = scrollY - gridY;
+ const bottomY = scrollY + viewportHeight - gridY;
+
+ if (bottomY < 0) return;
+
+ const topRow = Math.max(0, Math.floor(topY / wallpaperGrid.cellHeight));
+ const bottomRow = Math.floor(bottomY / wallpaperGrid.cellHeight);
+
+ // Update visible count with 1 row buffer ahead
+ const bufferRows = 1;
+ const neededBottomRow = bottomRow + bufferRows;
+ const neededCount = Math.min((neededBottomRow + 1) * wallpaperGrid.columnsCount, lazyModel.loadedCount);
+ lazyModel.updateVisibleCount(neededCount);
+
+ // Load more when we're within 1 row of running out of loaded items
+ const loadedRows = Math.ceil(lazyModel.loadedCount / wallpaperGrid.columnsCount);
+ const rowsRemaining = loadedRows - (bottomRow + 1);
+
+ if (rowsRemaining <= 1 && lazyModel.loadedCount < lazyModel.totalCount) {
+ if (!wallpaperGrid._expansionInProgress) {
+ wallpaperGrid._expansionInProgress = true;
+ lazyModel.loadOneRow();
+ Qt.callLater(function() {
+ wallpaperGrid._expansionInProgress = false;
+ });
+ }
+ }
+ }
+ }
+
+ // Fallback timer to check scroll position periodically
+ Timer {
+ id: scrollCheckTimer
+ interval: 100
+ running: {
+ const isActive = root.session.activeIndex === 3;
+ return isActive && lazyModel && lazyModel.sourceList && lazyModel.loadedCount < lazyModel.totalCount;
+ }
+ repeat: true
+ onTriggered: {
+ // Double-check that appearance pane is still active
+ const isActive = root.session.activeIndex === 3;
+ if (!isActive) {
+ stop();
+ return;
+ }
+
+ const flickable = wallpaperGridContainer.parentFlickable;
+ if (!flickable || !lazyModel || !lazyModel.sourceList) return;
+
+ const gridY = wallpaperGridContainer.y;
+ const scrollY = flickable.contentY;
+ const viewportHeight = flickable.height;
+
+ const topY = scrollY - gridY;
+ const bottomY = scrollY + viewportHeight - gridY;
+ if (bottomY < 0) return;
+
+ const topRow = Math.max(0, Math.floor(topY / wallpaperGrid.cellHeight));
+ const bottomRow = Math.floor(bottomY / wallpaperGrid.cellHeight);
+
+ const bufferRows = 1;
+ const neededBottomRow = bottomRow + bufferRows;
+ const neededCount = Math.min((neededBottomRow + 1) * wallpaperGrid.columnsCount, lazyModel.loadedCount);
+ lazyModel.updateVisibleCount(neededCount);
+
+ // Load more when we're within 1 row of running out of loaded items
+ const loadedRows = Math.ceil(lazyModel.loadedCount / wallpaperGrid.columnsCount);
+ const rowsRemaining = loadedRows - (bottomRow + 1);
+
+ if (rowsRemaining <= 1 && lazyModel.loadedCount < lazyModel.totalCount) {
+ if (!wallpaperGrid._expansionInProgress) {
+ wallpaperGrid._expansionInProgress = true;
+ lazyModel.loadOneRow();
+ Qt.callLater(function() {
+ wallpaperGrid._expansionInProgress = false;
+ });
+ }
+ }
+ }
+ }
+
+
+ // Parent Flickable handles scrolling
+ interactive: false
+
+
+ delegate: Item {
+ required property var modelData
+
+ width: wallpaperGrid.cellWidth
+ height: wallpaperGrid.cellHeight
+
+ readonly property bool isCurrent: modelData.path === Wallpapers.actualCurrent
+ readonly property real itemMargin: Appearance.spacing.normal / 2
+ readonly property real itemRadius: Appearance.rounding.normal
+
+ Component.onCompleted: {
+ wallpaperGrid._delegateCount++;
+ }
+
+ StateLayer {
+ anchors.fill: parent
+ anchors.leftMargin: itemMargin
+ anchors.rightMargin: itemMargin
+ anchors.topMargin: itemMargin
+ anchors.bottomMargin: itemMargin
+ radius: itemRadius
+
+ function onClicked(): void {
+ Wallpapers.setWallpaper(modelData.path);
+ }
+ }
+
+ StyledClippingRect {
+ id: image
+
+ anchors.fill: parent
+ anchors.leftMargin: itemMargin
+ anchors.rightMargin: itemMargin
+ anchors.topMargin: itemMargin
+ anchors.bottomMargin: itemMargin
+ color: Colours.tPalette.m3surfaceContainer
+ radius: itemRadius
+ antialiasing: true
+ layer.enabled: true
+ layer.smooth: true
+
+ CachingImage {
+ id: cachingImage
+
+ path: modelData.path
+ anchors.fill: parent
+ fillMode: Image.PreserveAspectCrop
+ cache: true
+ visible: opacity > 0
+ antialiasing: true
+ smooth: true
+
+ opacity: status === Image.Ready ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 1000
+ easing.type: Easing.OutQuad
+ }
+ }
+ }
+
+ // Fallback if CachingImage fails to load
+ Image {
+ id: fallbackImage
+
+ anchors.fill: parent
+ source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : ""
+ asynchronous: true
+ fillMode: Image.PreserveAspectCrop
+ cache: true
+ visible: opacity > 0
+ antialiasing: true
+ smooth: true
+
+ opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 1000
+ easing.type: Easing.OutQuad
+ }
+ }
+ }
+
+ Timer {
+ id: fallbackTimer
+
+ property bool triggered: false
+ interval: 800
+ running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null
+ onTriggered: triggered = true
+ }
+
+ // Gradient overlay for filename
+ Rectangle {
+ id: filenameOverlay
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+
+ implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5
+ radius: 0
+
+ gradient: Gradient {
+ GradientStop {
+ position: 0.0
+ color: Qt.rgba(Colours.palette.m3surfaceContainer.r,
+ Colours.palette.m3surfaceContainer.g,
+ Colours.palette.m3surfaceContainer.b, 0)
+ }
+ GradientStop {
+ position: 0.3
+ color: Qt.rgba(Colours.palette.m3surfaceContainer.r,
+ Colours.palette.m3surfaceContainer.g,
+ Colours.palette.m3surfaceContainer.b, 0.7)
+ }
+ GradientStop {
+ position: 0.6
+ color: Qt.rgba(Colours.palette.m3surfaceContainer.r,
+ Colours.palette.m3surfaceContainer.g,
+ Colours.palette.m3surfaceContainer.b, 0.9)
+ }
+ GradientStop {
+ position: 1.0
+ color: Qt.rgba(Colours.palette.m3surfaceContainer.r,
+ Colours.palette.m3surfaceContainer.g,
+ Colours.palette.m3surfaceContainer.b, 0.95)
+ }
+ }
+
+ opacity: 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 1000
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ Component.onCompleted: {
+ opacity = 1;
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ anchors.leftMargin: itemMargin
+ anchors.rightMargin: itemMargin
+ anchors.topMargin: itemMargin
+ anchors.bottomMargin: itemMargin
+ color: "transparent"
+ radius: itemRadius + border.width
+ border.width: isCurrent ? 2 : 0
+ border.color: Colours.palette.m3primary
+ antialiasing: true
+ smooth: true
+
+ Behavior on border.width {
+ NumberAnimation {
+ duration: 150
+ easing.type: Easing.OutQuad
+ }
+ }
+
+ MaterialIcon {
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Appearance.padding.small
+
+ visible: isCurrent
+ text: "check_circle"
+ color: Colours.palette.m3primary
+ font.pointSize: Appearance.font.size.large
+ }
+ }
+
+ StyledText {
+ id: filenameText
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
+ anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
+ anchors.bottomMargin: Appearance.padding.normal
+
+ readonly property string fileName: {
+ const path = modelData.relativePath || "";
+ const parts = path.split("/");
+ return parts.length > 0 ? parts[parts.length - 1] : path;
+ }
+
+ text: fileName
+ font.pointSize: Appearance.font.size.smaller
+ font.weight: 500
+ color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface
+ elide: Text.ElideMiddle
+ maximumLineCount: 1
+ horizontalAlignment: Text.AlignHCenter
+
+ opacity: 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 1000
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ Component.onCompleted: {
+ opacity = 1;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
+ }
+
+ SplitPaneLayout {
+ anchors.fill: parent
- Component {
- id: appearanceLeftContentComponent
+ leftContent: Component {
StyledFlickable {
id: sidebarFlickable
- readonly property var rootPane: leftAppearanceLoader.rootPane
+ readonly property var rootPane: root
flickableDirection: Flickable.VerticalFlick
contentHeight: sidebarLayout.height
@@ -1808,651 +2387,9 @@ RowLayout {
}
}
}
- }
}
- }
-
- Item {
- Layout.fillWidth: true
- Layout.fillHeight: true
-
- ClippingRectangle {
- id: rightAppearanceClippingRect
- anchors.fill: parent
- anchors.margins: Appearance.padding.normal
- anchors.leftMargin: 0
- anchors.rightMargin: Appearance.padding.normal / 2
- radius: rightAppearanceBorder.innerRadius
- color: "transparent"
-
- Loader {
- id: rightAppearanceLoader
- anchors.fill: parent
- anchors.margins: Appearance.padding.large * 2
- asynchronous: true
- sourceComponent: appearanceRightContentComponent
- property var rootPane: root
-
- onStatusChanged: {
- if (status === Loader.Error) {
- console.error("[AppearancePane] Right appearance loader error!");
- }
- }
- }
}
- InnerBorder {
- id: rightAppearanceBorder
- leftThickness: Appearance.padding.normal / 2
- }
-
- Component {
- id: appearanceRightContentComponent
-
- StyledFlickable {
- id: rightAppearanceFlickable
- flickableDirection: Flickable.VerticalFlick
- contentHeight: contentLayout.height
-
- StyledScrollBar.vertical: StyledScrollBar {
- flickable: rightAppearanceFlickable
- }
-
- ColumnLayout {
- id: contentLayout
-
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.top: parent.top
- spacing: Appearance.spacing.normal
-
- MaterialIcon {
- Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
- Layout.topMargin: 0
- text: "palette"
- font.pointSize: Appearance.font.size.extraLarge * 3
- font.bold: true
- }
-
- StyledText {
- Layout.alignment: Qt.AlignHCenter
- text: qsTr("Appearance Settings")
- font.pointSize: Appearance.font.size.large
- font.bold: true
- }
-
- StyledText {
- Layout.topMargin: Appearance.spacing.large
- Layout.alignment: Qt.AlignHCenter
- text: qsTr("Wallpaper")
- font.pointSize: Appearance.font.size.extraLarge
- font.weight: 600
- }
-
- StyledText {
- Layout.alignment: Qt.AlignHCenter
- text: qsTr("Select a wallpaper")
- font.pointSize: Appearance.font.size.normal
- color: Colours.palette.m3onSurfaceVariant
- }
-
- Item {
- Layout.fillWidth: true
- Layout.topMargin: Appearance.spacing.large
- Layout.preferredHeight: wallpaperLoader.item ? wallpaperLoader.item.layoutPreferredHeight : 0
-
- Loader {
- id: wallpaperLoader
- anchors.fill: parent
- asynchronous: true
- active: {
- // Lazy load: only activate when:
- // 1. Right pane is loaded AND
- // 2. Appearance pane is active (index 3) or adjacent (for smooth transitions)
- // This prevents loading all wallpapers when control center opens but appearance pane isn't visible
- const isActive = root.session.activeIndex === 3;
- const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1;
- const shouldActivate = rightAppearanceLoader.item !== null && (isActive || isAdjacent);
- return shouldActivate;
- }
-
- onStatusChanged: {
- if (status === Loader.Error) {
- console.error("[AppearancePane] Wallpaper loader error!");
- }
- }
-
- // Stop lazy loading when loader becomes inactive
- onActiveChanged: {
- if (!active && wallpaperLoader.item) {
- const container = wallpaperLoader.item;
- // Access timer through wallpaperGrid
- if (container && container.wallpaperGrid) {
- if (container.wallpaperGrid.scrollCheckTimer) {
- container.wallpaperGrid.scrollCheckTimer.stop();
- }
- container.wallpaperGrid._expansionInProgress = false;
- }
- }
- }
-
- sourceComponent: Item {
- id: wallpaperGridContainer
- property alias layoutPreferredHeight: wallpaperGrid.layoutPreferredHeight
-
- // Find and store reference to parent Flickable for scroll monitoring
- property var parentFlickable: {
- let item = parent;
- while (item) {
- if (item.flickableDirection !== undefined) {
- return item;
- }
- item = item.parent;
- }
- return null;
- }
-
- // Cleanup when component is destroyed
- Component.onDestruction: {
- if (wallpaperGrid) {
- if (wallpaperGrid.scrollCheckTimer) {
- wallpaperGrid.scrollCheckTimer.stop();
- }
- wallpaperGrid._expansionInProgress = false;
- }
- }
-
- // Lazy loading model: loads one image at a time, only when touching bottom
- // This prevents GridView from creating all delegates at once
- QtObject {
- id: lazyModel
-
- property var sourceList: null
- property int loadedCount: 0 // Total items available to load
- property int visibleCount: 0 // Items actually exposed to GridView (only visible + buffer)
- property int totalCount: 0
-
- function initialize(list) {
- sourceList = list;
- totalCount = list ? list.length : 0;
- // Start with enough items to fill the initial viewport (~3 rows)
- const initialRows = 3;
- const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 3;
- const initialCount = Math.min(initialRows * cols, totalCount);
- loadedCount = initialCount;
- visibleCount = initialCount;
- }
-
- function loadOneRow() {
- if (loadedCount < totalCount) {
- const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 1;
- const itemsToLoad = Math.min(cols, totalCount - loadedCount);
- loadedCount += itemsToLoad;
- }
- }
-
- function updateVisibleCount(neededCount) {
- // Always round up to complete rows to avoid incomplete rows in the grid
- const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 1;
- const maxVisible = Math.min(neededCount, loadedCount);
- const rows = Math.ceil(maxVisible / cols);
- const newVisibleCount = Math.min(rows * cols, loadedCount);
-
- if (newVisibleCount > visibleCount) {
- visibleCount = newVisibleCount;
- }
- }
- }
-
- GridView {
- id: wallpaperGrid
- anchors.fill: parent
-
- property int _delegateCount: 0
-
- readonly property int minCellWidth: 200 + Appearance.spacing.normal
- readonly property int columnsCount: Math.max(1, Math.floor(parent.width / minCellWidth))
-
- // Height based on visible items only - prevents GridView from creating all delegates
- readonly property int layoutPreferredHeight: {
- if (!lazyModel || lazyModel.visibleCount === 0 || columnsCount === 0) {
- return 0;
- }
- const calculated = Math.ceil(lazyModel.visibleCount / columnsCount) * cellHeight;
- return calculated;
- }
-
- height: layoutPreferredHeight
- cellWidth: width / columnsCount
- cellHeight: 140 + Appearance.spacing.normal
-
- leftMargin: 0
- rightMargin: 0
- topMargin: 0
- bottomMargin: 0
-
- // Use ListModel for incremental updates to prevent flashing when new items are added
- ListModel {
- id: wallpaperListModel
- }
-
- model: wallpaperListModel
-
- Connections {
- target: lazyModel
- function onVisibleCountChanged(): void {
- if (!lazyModel || !lazyModel.sourceList) return;
-
- const newCount = lazyModel.visibleCount;
- const currentCount = wallpaperListModel.count;
-
- // Only append new items - never remove or replace existing ones
- if (newCount > currentCount) {
- const flickable = wallpaperGridContainer.parentFlickable;
- const oldScrollY = flickable ? flickable.contentY : 0;
-
- for (let i = currentCount; i < newCount; i++) {
- wallpaperListModel.append({modelData: lazyModel.sourceList[i]});
- }
-
- // Preserve scroll position after model update
- if (flickable) {
- Qt.callLater(function() {
- if (Math.abs(flickable.contentY - oldScrollY) < 1) {
- flickable.contentY = oldScrollY;
- }
- });
- }
- }
- }
- }
-
- Component.onCompleted: {
- Qt.callLater(function() {
- const isActive = root.session.activeIndex === 3;
- if (width > 0 && parent && parent.visible && isActive && Wallpapers.list) {
- lazyModel.initialize(Wallpapers.list);
- wallpaperListModel.clear();
- for (let i = 0; i < lazyModel.visibleCount; i++) {
- wallpaperListModel.append({modelData: lazyModel.sourceList[i]});
- }
- }
- });
- }
-
- Connections {
- target: root.session
- function onActiveIndexChanged(): void {
- const isActive = root.session.activeIndex === 3;
-
- // Stop lazy loading when switching away from appearance pane
- if (!isActive) {
- if (scrollCheckTimer) {
- scrollCheckTimer.stop();
- }
- if (wallpaperGrid) {
- wallpaperGrid._expansionInProgress = false;
- }
- return;
- }
-
- // Initialize if needed when switching to appearance pane
- if (isActive && width > 0 && !lazyModel.sourceList && parent && parent.visible && Wallpapers.list) {
- lazyModel.initialize(Wallpapers.list);
- wallpaperListModel.clear();
- for (let i = 0; i < lazyModel.visibleCount; i++) {
- wallpaperListModel.append({modelData: lazyModel.sourceList[i]});
- }
- }
- }
- }
-
- onWidthChanged: {
- const isActive = root.session.activeIndex === 3;
- if (width > 0 && !lazyModel.sourceList && parent && parent.visible && isActive && Wallpapers.list) {
- lazyModel.initialize(Wallpapers.list);
- wallpaperListModel.clear();
- for (let i = 0; i < lazyModel.visibleCount; i++) {
- wallpaperListModel.append({modelData: lazyModel.sourceList[i]});
- }
- }
- }
-
- // Force true lazy loading: only create delegates for visible items
- displayMarginBeginning: 0
- displayMarginEnd: 0
- cacheBuffer: 0
-
- // Debounce expansion to avoid too frequent checks
- property bool _expansionInProgress: false
-
- Connections {
- target: wallpaperGridContainer.parentFlickable
- function onContentYChanged(): void {
- // Don't process scroll events if appearance pane is not active
- const isActive = root.session.activeIndex === 3;
- if (!isActive) return;
-
- if (!lazyModel || !lazyModel.sourceList || lazyModel.loadedCount >= lazyModel.totalCount || wallpaperGrid._expansionInProgress) {
- return;
- }
-
- const flickable = wallpaperGridContainer.parentFlickable;
- if (!flickable) return;
-
- const gridY = wallpaperGridContainer.y;
- const scrollY = flickable.contentY;
- const viewportHeight = flickable.height;
-
- const topY = scrollY - gridY;
- const bottomY = scrollY + viewportHeight - gridY;
-
- if (bottomY < 0) return;
-
- const topRow = Math.max(0, Math.floor(topY / wallpaperGrid.cellHeight));
- const bottomRow = Math.floor(bottomY / wallpaperGrid.cellHeight);
-
- // Update visible count with 1 row buffer ahead
- const bufferRows = 1;
- const neededBottomRow = bottomRow + bufferRows;
- const neededCount = Math.min((neededBottomRow + 1) * wallpaperGrid.columnsCount, lazyModel.loadedCount);
- lazyModel.updateVisibleCount(neededCount);
-
- // Load more when we're within 1 row of running out of loaded items
- const loadedRows = Math.ceil(lazyModel.loadedCount / wallpaperGrid.columnsCount);
- const rowsRemaining = loadedRows - (bottomRow + 1);
-
- if (rowsRemaining <= 1 && lazyModel.loadedCount < lazyModel.totalCount) {
- if (!wallpaperGrid._expansionInProgress) {
- wallpaperGrid._expansionInProgress = true;
- lazyModel.loadOneRow();
- Qt.callLater(function() {
- wallpaperGrid._expansionInProgress = false;
- });
- }
- }
- }
- }
-
- // Fallback timer to check scroll position periodically
- Timer {
- id: scrollCheckTimer
- interval: 100
- running: {
- const isActive = root.session.activeIndex === 3;
- return isActive && lazyModel && lazyModel.sourceList && lazyModel.loadedCount < lazyModel.totalCount;
- }
- repeat: true
- onTriggered: {
- // Double-check that appearance pane is still active
- const isActive = root.session.activeIndex === 3;
- if (!isActive) {
- stop();
- return;
- }
-
- const flickable = wallpaperGridContainer.parentFlickable;
- if (!flickable || !lazyModel || !lazyModel.sourceList) return;
-
- const gridY = wallpaperGridContainer.y;
- const scrollY = flickable.contentY;
- const viewportHeight = flickable.height;
-
- const topY = scrollY - gridY;
- const bottomY = scrollY + viewportHeight - gridY;
- if (bottomY < 0) return;
-
- const topRow = Math.max(0, Math.floor(topY / wallpaperGrid.cellHeight));
- const bottomRow = Math.floor(bottomY / wallpaperGrid.cellHeight);
-
- const bufferRows = 1;
- const neededBottomRow = bottomRow + bufferRows;
- const neededCount = Math.min((neededBottomRow + 1) * wallpaperGrid.columnsCount, lazyModel.loadedCount);
- lazyModel.updateVisibleCount(neededCount);
-
- // Load more when we're within 1 row of running out of loaded items
- const loadedRows = Math.ceil(lazyModel.loadedCount / wallpaperGrid.columnsCount);
- const rowsRemaining = loadedRows - (bottomRow + 1);
-
- if (rowsRemaining <= 1 && lazyModel.loadedCount < lazyModel.totalCount) {
- if (!wallpaperGrid._expansionInProgress) {
- wallpaperGrid._expansionInProgress = true;
- lazyModel.loadOneRow();
- Qt.callLater(function() {
- wallpaperGrid._expansionInProgress = false;
- });
- }
- }
- }
- }
-
-
- // Parent Flickable handles scrolling
- interactive: false
-
-
- delegate: Item {
- required property var modelData
-
- width: wallpaperGrid.cellWidth
- height: wallpaperGrid.cellHeight
-
- readonly property bool isCurrent: modelData.path === Wallpapers.actualCurrent
- readonly property real itemMargin: Appearance.spacing.normal / 2
- readonly property real itemRadius: Appearance.rounding.normal
-
- Component.onCompleted: {
- wallpaperGrid._delegateCount++;
- }
-
- StateLayer {
- anchors.fill: parent
- anchors.leftMargin: itemMargin
- anchors.rightMargin: itemMargin
- anchors.topMargin: itemMargin
- anchors.bottomMargin: itemMargin
- radius: itemRadius
-
- function onClicked(): void {
- Wallpapers.setWallpaper(modelData.path);
- }
- }
-
- StyledClippingRect {
- id: image
-
- anchors.fill: parent
- anchors.leftMargin: itemMargin
- anchors.rightMargin: itemMargin
- anchors.topMargin: itemMargin
- anchors.bottomMargin: itemMargin
- color: Colours.tPalette.m3surfaceContainer
- radius: itemRadius
- antialiasing: true
- layer.enabled: true
- layer.smooth: true
-
- CachingImage {
- id: cachingImage
-
- path: modelData.path
- anchors.fill: parent
- fillMode: Image.PreserveAspectCrop
- cache: true
- visible: opacity > 0
- antialiasing: true
- smooth: true
-
- opacity: status === Image.Ready ? 1 : 0
-
- Behavior on opacity {
- NumberAnimation {
- duration: 1000
- easing.type: Easing.OutQuad
- }
- }
- }
-
- // Fallback if CachingImage fails to load
- Image {
- id: fallbackImage
-
- anchors.fill: parent
- source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : ""
- asynchronous: true
- fillMode: Image.PreserveAspectCrop
- cache: true
- visible: opacity > 0
- antialiasing: true
- smooth: true
-
- opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0
-
- Behavior on opacity {
- NumberAnimation {
- duration: 1000
- easing.type: Easing.OutQuad
- }
- }
- }
-
- Timer {
- id: fallbackTimer
-
- property bool triggered: false
- interval: 800
- running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null
- onTriggered: triggered = true
- }
-
- // Gradient overlay for filename
- Rectangle {
- id: filenameOverlay
-
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.bottom: parent.bottom
-
- implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5
- radius: 0
-
- gradient: Gradient {
- GradientStop {
- position: 0.0
- color: Qt.rgba(Colours.palette.m3surfaceContainer.r,
- Colours.palette.m3surfaceContainer.g,
- Colours.palette.m3surfaceContainer.b, 0)
- }
- GradientStop {
- position: 0.3
- color: Qt.rgba(Colours.palette.m3surfaceContainer.r,
- Colours.palette.m3surfaceContainer.g,
- Colours.palette.m3surfaceContainer.b, 0.7)
- }
- GradientStop {
- position: 0.6
- color: Qt.rgba(Colours.palette.m3surfaceContainer.r,
- Colours.palette.m3surfaceContainer.g,
- Colours.palette.m3surfaceContainer.b, 0.9)
- }
- GradientStop {
- position: 1.0
- color: Qt.rgba(Colours.palette.m3surfaceContainer.r,
- Colours.palette.m3surfaceContainer.g,
- Colours.palette.m3surfaceContainer.b, 0.95)
- }
- }
-
- opacity: 0
-
- Behavior on opacity {
- NumberAnimation {
- duration: 1000
- easing.type: Easing.OutCubic
- }
- }
-
- Component.onCompleted: {
- opacity = 1;
- }
- }
- }
-
- Rectangle {
- anchors.fill: parent
- anchors.leftMargin: itemMargin
- anchors.rightMargin: itemMargin
- anchors.topMargin: itemMargin
- anchors.bottomMargin: itemMargin
- color: "transparent"
- radius: itemRadius + border.width
- border.width: isCurrent ? 2 : 0
- border.color: Colours.palette.m3primary
- antialiasing: true
- smooth: true
-
- Behavior on border.width {
- NumberAnimation {
- duration: 150
- easing.type: Easing.OutQuad
- }
- }
-
- MaterialIcon {
- anchors.right: parent.right
- anchors.top: parent.top
- anchors.margins: Appearance.padding.small
-
- visible: isCurrent
- text: "check_circle"
- color: Colours.palette.m3primary
- font.pointSize: Appearance.font.size.large
- }
- }
-
- StyledText {
- id: filenameText
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.bottom: parent.bottom
- anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
- anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
- anchors.bottomMargin: Appearance.padding.normal
-
- readonly property string fileName: {
- const path = modelData.relativePath || "";
- const parts = path.split("/");
- return parts.length > 0 ? parts[parts.length - 1] : path;
- }
-
- text: fileName
- font.pointSize: Appearance.font.size.smaller
- font.weight: 500
- color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface
- elide: Text.ElideMiddle
- maximumLineCount: 1
- horizontalAlignment: Text.AlignHCenter
-
- opacity: 0
-
- Behavior on opacity {
- NumberAnimation {
- duration: 1000
- easing.type: Easing.OutCubic
- }
- }
-
- Component.onCompleted: {
- opacity = 1;
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
+ rightContent: appearanceRightContentComponent
}
}
diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml
index c2d60d8..dc3ba56 100644
--- a/modules/controlcenter/audio/AudioPane.qml
+++ b/modules/controlcenter/audio/AudioPane.qml
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import ".."
+import "../components"
import qs.components
import qs.components.controls
import qs.components.effects
@@ -11,52 +12,17 @@ import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
-RowLayout {
+Item {
id: root
required property Session session
anchors.fill: parent
- spacing: 0
+ SplitPaneLayout {
+ anchors.fill: parent
- Item {
- id: leftAudioItem
- Layout.preferredWidth: Math.floor(parent.width * 0.4)
- Layout.minimumWidth: 420
- Layout.fillHeight: true
-
- ClippingRectangle {
- id: leftAudioClippingRect
- anchors.fill: parent
- anchors.margins: Appearance.padding.normal
- anchors.leftMargin: 0
- anchors.rightMargin: Appearance.padding.normal / 2
-
- radius: leftAudioBorder.innerRadius
- color: "transparent"
-
- Loader {
- id: leftAudioLoader
-
- anchors.fill: parent
- anchors.margins: Appearance.padding.large + Appearance.padding.normal
- anchors.leftMargin: Appearance.padding.large
- anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
-
- asynchronous: true
- sourceComponent: audioLeftContentComponent
- }
- }
-
- InnerBorder {
- id: leftAudioBorder
- leftThickness: 0
- rightThickness: Appearance.padding.normal / 2
- }
-
- Component {
- id: audioLeftContentComponent
+ leftContent: Component {
StyledFlickable {
id: leftAudioFlickable
@@ -246,349 +212,321 @@ RowLayout {
}
}
}
- }
}
- }
-
- Item {
- id: rightAudioItem
- Layout.fillWidth: true
- Layout.fillHeight: true
-
- ClippingRectangle {
- id: rightAudioClippingRect
- anchors.fill: parent
- anchors.margins: Appearance.padding.normal
- anchors.leftMargin: 0
- anchors.rightMargin: Appearance.padding.normal / 2
-
- radius: rightAudioBorder.innerRadius
- color: "transparent"
-
- Loader {
- id: rightAudioLoader
+ }
+ rightContent: Component {
+ Item {
anchors.fill: parent
anchors.topMargin: Appearance.padding.large * 2
anchors.bottomMargin: Appearance.padding.large * 2
anchors.leftMargin: 0
anchors.rightMargin: 0
- asynchronous: true
- sourceComponent: audioRightContentComponent
- }
- }
-
- InnerBorder {
- id: rightAudioBorder
- leftThickness: Appearance.padding.normal / 2
- }
-
- Component {
- id: audioRightContentComponent
+ StyledFlickable {
+ id: rightAudioFlickable
+ anchors.fill: parent
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: contentLayout.height
- StyledFlickable {
- id: rightAudioFlickable
- flickableDirection: Flickable.VerticalFlick
- contentHeight: contentLayout.height
-
- StyledScrollBar.vertical: StyledScrollBar {
- flickable: rightAudioFlickable
- }
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: rightAudioFlickable
+ }
- ColumnLayout {
- id: contentLayout
+ ColumnLayout {
+ id: contentLayout
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.leftMargin: Appearance.padding.large * 2
- anchors.rightMargin: Appearance.padding.large * 2
- spacing: Appearance.spacing.normal
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: Appearance.padding.large * 2
+ anchors.rightMargin: Appearance.padding.large * 2
+ spacing: Appearance.spacing.normal
- ConnectionHeader {
- icon: "volume_up"
- title: qsTr("Audio Settings")
- }
+ ConnectionHeader {
+ icon: "volume_up"
+ title: qsTr("Audio Settings")
+ }
- SectionHeader {
- title: qsTr("Output volume")
- description: qsTr("Control the volume of your output device")
- }
+ SectionHeader {
+ title: qsTr("Output volume")
+ description: qsTr("Control the volume of your output device")
+ }
- SectionContainer {
- contentSpacing: Appearance.spacing.normal
+ SectionContainer {
+ contentSpacing: Appearance.spacing.normal
- ColumnLayout {
- Layout.fillWidth: true
- spacing: Appearance.spacing.small
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.small
- RowLayout {
- Layout.fillWidth: true
- spacing: Appearance.spacing.normal
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.normal
- StyledText {
- text: qsTr("Volume")
- font.pointSize: Appearance.font.size.normal
- font.weight: 500
- }
+ StyledText {
+ text: qsTr("Volume")
+ font.pointSize: Appearance.font.size.normal
+ font.weight: 500
+ }
- Item {
- Layout.fillWidth: true
- }
+ Item {
+ Layout.fillWidth: true
+ }
- StyledRect {
- Layout.preferredWidth: 70
- implicitHeight: outputVolumeInput.implicitHeight + Appearance.padding.small * 2
- color: outputVolumeInputHover.containsMouse || outputVolumeInput.activeFocus
- ? Colours.layer(Colours.palette.m3surfaceContainer, 3)
- : Colours.layer(Colours.palette.m3surfaceContainer, 2)
- radius: Appearance.rounding.small
- border.width: 1
- border.color: outputVolumeInput.activeFocus
- ? Colours.palette.m3primary
- : Qt.alpha(Colours.palette.m3outline, 0.3)
- enabled: !Audio.muted
- opacity: enabled ? 1 : 0.5
+ StyledRect {
+ Layout.preferredWidth: 70
+ implicitHeight: outputVolumeInput.implicitHeight + Appearance.padding.small * 2
+ color: outputVolumeInputHover.containsMouse || outputVolumeInput.activeFocus
+ ? Colours.layer(Colours.palette.m3surfaceContainer, 3)
+ : Colours.layer(Colours.palette.m3surfaceContainer, 2)
+ radius: Appearance.rounding.small
+ border.width: 1
+ border.color: outputVolumeInput.activeFocus
+ ? Colours.palette.m3primary
+ : Qt.alpha(Colours.palette.m3outline, 0.3)
+ enabled: !Audio.muted
+ opacity: enabled ? 1 : 0.5
- Behavior on color { CAnim {} }
- Behavior on border.color { CAnim {} }
+ Behavior on color { CAnim {} }
+ Behavior on border.color { CAnim {} }
- MouseArea {
- id: outputVolumeInputHover
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.IBeamCursor
- acceptedButtons: Qt.NoButton
- }
+ MouseArea {
+ id: outputVolumeInputHover
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.IBeamCursor
+ acceptedButtons: Qt.NoButton
+ }
- StyledTextField {
- id: outputVolumeInput
- anchors.centerIn: parent
- width: parent.width - Appearance.padding.normal
- horizontalAlignment: TextInput.AlignHCenter
- validator: IntValidator { bottom: 0; top: 100 }
- enabled: !Audio.muted
-
- Component.onCompleted: {
- text = Math.round(Audio.volume * 100).toString();
- }
-
- Connections {
- target: Audio
- function onVolumeChanged() {
- if (!outputVolumeInput.activeFocus) {
- outputVolumeInput.text = Math.round(Audio.volume * 100).toString();
+ StyledTextField {
+ id: outputVolumeInput
+ anchors.centerIn: parent
+ width: parent.width - Appearance.padding.normal
+ horizontalAlignment: TextInput.AlignHCenter
+ validator: IntValidator { bottom: 0; top: 100 }
+ enabled: !Audio.muted
+
+ Component.onCompleted: {
+ text = Math.round(Audio.volume * 100).toString();
}
- }
- }
-
- onTextChanged: {
- if (activeFocus) {
- const val = parseInt(text);
- if (!isNaN(val) && val >= 0 && val <= 100) {
- Audio.setVolume(val / 100);
+
+ Connections {
+ target: Audio
+ function onVolumeChanged() {
+ if (!outputVolumeInput.activeFocus) {
+ outputVolumeInput.text = Math.round(Audio.volume * 100).toString();
+ }
+ }
+ }
+
+ onTextChanged: {
+ if (activeFocus) {
+ const val = parseInt(text);
+ if (!isNaN(val) && val >= 0 && val <= 100) {
+ Audio.setVolume(val / 100);
+ }
+ }
+ }
+ onEditingFinished: {
+ const val = parseInt(text);
+ if (isNaN(val) || val < 0 || val > 100) {
+ text = Math.round(Audio.volume * 100).toString();
+ }
}
}
}
- onEditingFinished: {
- const val = parseInt(text);
- if (isNaN(val) || val < 0 || val > 100) {
- text = Math.round(Audio.volume * 100).toString();
- }
+
+ StyledText {
+ text: "%"
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.normal
+ opacity: Audio.muted ? 0.5 : 1
}
- }
- }
- StyledText {
- text: "%"
- color: Colours.palette.m3outline
- font.pointSize: Appearance.font.size.normal
- opacity: Audio.muted ? 0.5 : 1
- }
+ StyledRect {
+ implicitWidth: implicitHeight
+ implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2
- StyledRect {
- implicitWidth: implicitHeight
- implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2
+ radius: Appearance.rounding.normal
+ color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
- radius: Appearance.rounding.normal
- color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
+ StateLayer {
+ function onClicked(): void {
+ if (Audio.sink?.audio) {
+ Audio.sink.audio.muted = !Audio.sink.audio.muted;
+ }
+ }
+ }
- StateLayer {
- function onClicked(): void {
- if (Audio.sink?.audio) {
- Audio.sink.audio.muted = !Audio.sink.audio.muted;
+ MaterialIcon {
+ id: muteIcon
+
+ anchors.centerIn: parent
+ text: Audio.muted ? "volume_off" : "volume_up"
+ color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
}
}
}
- MaterialIcon {
- id: muteIcon
+ StyledSlider {
+ id: outputVolumeSlider
+ Layout.fillWidth: true
+ implicitHeight: Appearance.padding.normal * 3
- anchors.centerIn: parent
- text: Audio.muted ? "volume_off" : "volume_up"
- color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
+ value: Audio.volume
+ enabled: !Audio.muted
+ opacity: enabled ? 1 : 0.5
+ onMoved: {
+ Audio.setVolume(value);
+ if (!outputVolumeInput.activeFocus) {
+ outputVolumeInput.text = Math.round(value * 100).toString();
+ }
+ }
}
}
}
- StyledSlider {
- id: outputVolumeSlider
- Layout.fillWidth: true
- implicitHeight: Appearance.padding.normal * 3
-
- value: Audio.volume
- enabled: !Audio.muted
- opacity: enabled ? 1 : 0.5
- onMoved: {
- Audio.setVolume(value);
- if (!outputVolumeInput.activeFocus) {
- outputVolumeInput.text = Math.round(value * 100).toString();
- }
- }
+ SectionHeader {
+ title: qsTr("Input volume")
+ description: qsTr("Control the volume of your input device")
}
- }
- }
- SectionHeader {
- title: qsTr("Input volume")
- description: qsTr("Control the volume of your input device")
- }
+ SectionContainer {
+ contentSpacing: Appearance.spacing.normal
- SectionContainer {
- contentSpacing: Appearance.spacing.normal
-
- ColumnLayout {
- Layout.fillWidth: true
- spacing: Appearance.spacing.small
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.small
- RowLayout {
- Layout.fillWidth: true
- spacing: Appearance.spacing.normal
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.normal
- StyledText {
- text: qsTr("Volume")
- font.pointSize: Appearance.font.size.normal
- font.weight: 500
- }
+ StyledText {
+ text: qsTr("Volume")
+ font.pointSize: Appearance.font.size.normal
+ font.weight: 500
+ }
- Item {
- Layout.fillWidth: true
- }
+ Item {
+ Layout.fillWidth: true
+ }
- StyledRect {
- Layout.preferredWidth: 70
- implicitHeight: inputVolumeInput.implicitHeight + Appearance.padding.small * 2
- color: inputVolumeInputHover.containsMouse || inputVolumeInput.activeFocus
- ? Colours.layer(Colours.palette.m3surfaceContainer, 3)
- : Colours.layer(Colours.palette.m3surfaceContainer, 2)
- radius: Appearance.rounding.small
- border.width: 1
- border.color: inputVolumeInput.activeFocus
- ? Colours.palette.m3primary
- : Qt.alpha(Colours.palette.m3outline, 0.3)
- enabled: !Audio.sourceMuted
- opacity: enabled ? 1 : 0.5
+ StyledRect {
+ Layout.preferredWidth: 70
+ implicitHeight: inputVolumeInput.implicitHeight + Appearance.padding.small * 2
+ color: inputVolumeInputHover.containsMouse || inputVolumeInput.activeFocus
+ ? Colours.layer(Colours.palette.m3surfaceContainer, 3)
+ : Colours.layer(Colours.palette.m3surfaceContainer, 2)
+ radius: Appearance.rounding.small
+ border.width: 1
+ border.color: inputVolumeInput.activeFocus
+ ? Colours.palette.m3primary
+ : Qt.alpha(Colours.palette.m3outline, 0.3)
+ enabled: !Audio.sourceMuted
+ opacity: enabled ? 1 : 0.5
- Behavior on color { CAnim {} }
- Behavior on border.color { CAnim {} }
+ Behavior on color { CAnim {} }
+ Behavior on border.color { CAnim {} }
- MouseArea {
- id: inputVolumeInputHover
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.IBeamCursor
- acceptedButtons: Qt.NoButton
- }
+ MouseArea {
+ id: inputVolumeInputHover
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.IBeamCursor
+ acceptedButtons: Qt.NoButton
+ }
- StyledTextField {
- id: inputVolumeInput
- anchors.centerIn: parent
- width: parent.width - Appearance.padding.normal
- horizontalAlignment: TextInput.AlignHCenter
- validator: IntValidator { bottom: 0; top: 100 }
- enabled: !Audio.sourceMuted
-
- Component.onCompleted: {
- text = Math.round(Audio.sourceVolume * 100).toString();
- }
-
- Connections {
- target: Audio
- function onSourceVolumeChanged() {
- if (!inputVolumeInput.activeFocus) {
- inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString();
+ StyledTextField {
+ id: inputVolumeInput
+ anchors.centerIn: parent
+ width: parent.width - Appearance.padding.normal
+ horizontalAlignment: TextInput.AlignHCenter
+ validator: IntValidator { bottom: 0; top: 100 }
+ enabled: !Audio.sourceMuted
+
+ Component.onCompleted: {
+ text = Math.round(Audio.sourceVolume * 100).toString();
}
- }
- }
-
- onTextChanged: {
- if (activeFocus) {
- const val = parseInt(text);
- if (!isNaN(val) && val >= 0 && val <= 100) {
- Audio.setSourceVolume(val / 100);
+
+ Connections {
+ target: Audio
+ function onSourceVolumeChanged() {
+ if (!inputVolumeInput.activeFocus) {
+ inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString();
+ }
+ }
+ }
+
+ onTextChanged: {
+ if (activeFocus) {
+ const val = parseInt(text);
+ if (!isNaN(val) && val >= 0 && val <= 100) {
+ Audio.setSourceVolume(val / 100);
+ }
+ }
+ }
+ onEditingFinished: {
+ const val = parseInt(text);
+ if (isNaN(val) || val < 0 || val > 100) {
+ text = Math.round(Audio.sourceVolume * 100).toString();
+ }
}
}
}
- onEditingFinished: {
- const val = parseInt(text);
- if (isNaN(val) || val < 0 || val > 100) {
- text = Math.round(Audio.sourceVolume * 100).toString();
- }
- }
- }
- }
- StyledText {
- text: "%"
- color: Colours.palette.m3outline
- font.pointSize: Appearance.font.size.normal
- opacity: Audio.sourceMuted ? 0.5 : 1
- }
+ StyledText {
+ text: "%"
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.normal
+ opacity: Audio.sourceMuted ? 0.5 : 1
+ }
- StyledRect {
- implicitWidth: implicitHeight
- implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2
+ StyledRect {
+ implicitWidth: implicitHeight
+ implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2
- radius: Appearance.rounding.normal
- color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
+ radius: Appearance.rounding.normal
+ color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
- StateLayer {
- function onClicked(): void {
- if (Audio.source?.audio) {
- Audio.source.audio.muted = !Audio.source.audio.muted;
+ StateLayer {
+ function onClicked(): void {
+ if (Audio.source?.audio) {
+ Audio.source.audio.muted = !Audio.source.audio.muted;
+ }
+ }
}
- }
- }
- MaterialIcon {
- id: muteInputIcon
+ MaterialIcon {
+ id: muteInputIcon
- anchors.centerIn: parent
- text: "mic_off"
- color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
+ anchors.centerIn: parent
+ text: "mic_off"
+ color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
+ }
+ }
}
- }
- }
- StyledSlider {
- id: inputVolumeSlider
- Layout.fillWidth: true
- implicitHeight: Appearance.padding.normal * 3
+ StyledSlider {
+ id: inputVolumeSlider
+ Layout.fillWidth: true
+ implicitHeight: Appearance.padding.normal * 3
- value: Audio.sourceVolume
- enabled: !Audio.sourceMuted
- opacity: enabled ? 1 : 0.5
- onMoved: {
- Audio.setSourceVolume(value);
- if (!inputVolumeInput.activeFocus) {
- inputVolumeInput.text = Math.round(value * 100).toString();
+ value: Audio.sourceVolume
+ enabled: !Audio.sourceMuted
+ opacity: enabled ? 1 : 0.5
+ onMoved: {
+ Audio.setSourceVolume(value);
+ if (!inputVolumeInput.activeFocus) {
+ inputVolumeInput.text = Math.round(value * 100).toString();
+ }
+ }
}
}
}
}
}
}
- }
}
}
} \ No newline at end of file
diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml
index 8ad4b1f..cacb611 100644
--- a/modules/controlcenter/bluetooth/BtPane.qml
+++ b/modules/controlcenter/bluetooth/BtPane.qml
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import ".."
+import "../components"
import qs.components.controls
import qs.components.effects
import qs.components.containers
@@ -10,95 +11,50 @@ import Quickshell.Bluetooth
import QtQuick
import QtQuick.Layouts
-RowLayout {
+Item {
id: root
required property Session session
anchors.fill: parent
- spacing: 0
-
- Item {
- id: leftBtItem
- Layout.preferredWidth: Math.floor(parent.width * 0.4)
- Layout.minimumWidth: 420
- Layout.fillHeight: true
-
- ClippingRectangle {
- id: leftBtClippingRect
- anchors.fill: parent
- anchors.margins: Appearance.padding.normal
- anchors.leftMargin: 0
- anchors.rightMargin: Appearance.padding.normal / 2
-
- radius: leftBtBorder.innerRadius
- color: "transparent"
-
- Loader {
- id: leftBtLoader
-
- anchors.fill: parent
- anchors.margins: Appearance.padding.large + Appearance.padding.normal
- anchors.leftMargin: Appearance.padding.large
- anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
-
- asynchronous: true
- sourceComponent: btDeviceListComponent
- }
- }
-
- InnerBorder {
- id: leftBtBorder
- leftThickness: 0
- rightThickness: Appearance.padding.normal / 2
- }
-
- Component {
- id: btDeviceListComponent
+ SplitPaneLayout {
+ anchors.fill: parent
+ leftContent: Component {
DeviceList {
anchors.fill: parent
session: root.session
}
}
- }
- Item {
- id: rightBtItem
- Layout.fillWidth: true
- Layout.fillHeight: true
-
- ClippingRectangle {
- id: btClippingRect
- anchors.fill: parent
- anchors.margins: Appearance.padding.normal
- anchors.leftMargin: 0
- anchors.rightMargin: Appearance.padding.normal / 2
-
- radius: rightBorder.innerRadius
- color: "transparent"
-
- Loader {
- id: loader
+ rightContent: Component {
+ Item {
+ id: rightBtPane
property BluetoothDevice pane: root.session.bt.active
- anchors.fill: parent
- anchors.margins: Appearance.padding.large * 2
+ Loader {
+ id: rightLoader
- asynchronous: true
- sourceComponent: pane ? details : settings
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.large * 2
+
+ asynchronous: true
+ sourceComponent: rightBtPane.pane ? details : settings
+ }
Behavior on pane {
SequentialAnimation {
ParallelAnimation {
Anim {
+ target: rightLoader
property: "opacity"
to: 0
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
Anim {
+ target: rightLoader
property: "scale"
to: 0.8
easing.bezierCurve: Appearance.anim.curves.standardAccel
@@ -107,11 +63,13 @@ RowLayout {
PropertyAction {}
ParallelAnimation {
Anim {
+ target: rightLoader
property: "opacity"
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
+ target: rightLoader
property: "scale"
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
@@ -119,49 +77,49 @@ RowLayout {
}
}
}
- }
- }
- InnerBorder {
- id: rightBorder
-
- leftThickness: Appearance.padding.normal / 2
+ Connections {
+ target: root.session.bt
+ function onActiveChanged() {
+ rightBtPane.pane = root.session.bt.active;
+ }
+ }
+ }
}
+ }
- Component {
- id: settings
+ Component {
+ id: settings
- StyledFlickable {
- id: settingsFlickable
- flickableDirection: Flickable.VerticalFlick
- contentHeight: settingsInner.height
+ StyledFlickable {
+ id: settingsFlickable
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: settingsInner.height
- StyledScrollBar.vertical: StyledScrollBar {
- flickable: settingsFlickable
- }
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: settingsFlickable
+ }
- Settings {
- id: settingsInner
+ Settings {
+ id: settingsInner
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.top: parent.top
- session: root.session
- }
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ session: root.session
}
}
+ }
- Component {
- id: details
+ Component {
+ id: details
- Details {
- session: root.session
- }
+ Details {
+ session: root.session
}
}
component Anim: NumberAnimation {
- target: loader
duration: Appearance.anim.durations.normal / 2
easing.type: Easing.BezierSpline
}
diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml
new file mode 100644
index 0000000..7bd7db0
--- /dev/null
+++ b/modules/controlcenter/components/SplitPaneLayout.qml
@@ -0,0 +1,120 @@
+pragma ComponentBehavior: Bound
+
+import qs.components
+import qs.components.effects
+import qs.config
+import Quickshell.Widgets
+import QtQuick
+import QtQuick.Layouts
+
+RowLayout {
+ id: root
+
+ spacing: 0
+
+ property Component leftContent: null
+ property Component rightContent: null
+
+ // Left pane configuration
+ property real leftWidthRatio: 0.4
+ property int leftMinimumWidth: 420
+ property var leftLoaderProperties: ({})
+
+ // Right pane configuration
+ property var rightLoaderProperties: ({})
+
+ // Expose loaders for customization (access via splitLayout.leftLoader or splitLayout.rightLoader)
+ property alias leftLoader: leftLoader
+ property alias rightLoader: rightLoader
+
+ // Left pane
+ Item {
+ id: leftPane
+
+ Layout.preferredWidth: Math.floor(parent.width * root.leftWidthRatio)
+ Layout.minimumWidth: root.leftMinimumWidth
+ Layout.fillHeight: true
+
+ ClippingRectangle {
+ id: leftClippingRect
+
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.normal
+ anchors.leftMargin: 0
+ anchors.rightMargin: Appearance.padding.normal / 2
+
+ radius: leftBorder.innerRadius
+ color: "transparent"
+
+ Loader {
+ id: leftLoader
+
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.large + Appearance.padding.normal
+ anchors.leftMargin: Appearance.padding.large
+ anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
+
+ asynchronous: true
+ sourceComponent: root.leftContent
+
+ // Apply any additional properties from leftLoaderProperties
+ Component.onCompleted: {
+ for (const key in root.leftLoaderProperties) {
+ leftLoader[key] = root.leftLoaderProperties[key];
+ }
+ }
+ }
+ }
+
+ InnerBorder {
+ id: leftBorder
+
+ leftThickness: 0
+ rightThickness: Appearance.padding.normal / 2
+ }
+ }
+
+ // Right pane
+ Item {
+ id: rightPane
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ClippingRectangle {
+ id: rightClippingRect
+
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.normal
+ anchors.leftMargin: 0
+ anchors.rightMargin: Appearance.padding.normal / 2
+
+ radius: rightBorder.innerRadius
+ color: "transparent"
+
+ Loader {
+ id: rightLoader
+
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.large * 2
+
+ asynchronous: true
+ sourceComponent: root.rightContent
+
+ // Apply any additional properties from rightLoaderProperties
+ Component.onCompleted: {
+ for (const key in root.rightLoaderProperties) {
+ rightLoader[key] = root.rightLoaderProperties[key];
+ }
+ }
+ }
+ }
+
+ InnerBorder {
+ id: rightBorder
+
+ leftThickness: Appearance.padding.normal / 2
+ }
+ }
+}
+
diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml
index f2247a7..30e2953 100644
--- a/modules/controlcenter/launcher/LauncherPane.qml
+++ b/modules/controlcenter/launcher/LauncherPane.qml
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import ".."
+import "../components"
import "../../launcher/services"
import qs.components
import qs.components.controls
@@ -16,7 +17,7 @@ import QtQuick
import QtQuick.Layouts
import "../../../utils/scripts/fuzzysort.js" as Fuzzy
-RowLayout {
+Item {
id: root
required property Session session
@@ -26,8 +27,6 @@ RowLayout {
anchors.fill: parent
- spacing: 0
-
onSelectedAppChanged: {
root.session.launcher.active = root.selectedApp;
updateToggleState();
@@ -156,43 +155,10 @@ RowLayout {
}
}
- Item {
- id: leftLauncherItem
- Layout.preferredWidth: Math.floor(parent.width * 0.4)
- Layout.minimumWidth: 420
- Layout.fillHeight: true
-
- ClippingRectangle {
- id: leftLauncherClippingRect
- anchors.fill: parent
- anchors.margins: Appearance.padding.normal
- anchors.leftMargin: 0
- anchors.rightMargin: Appearance.padding.normal / 2
-
- radius: leftLauncherBorder.innerRadius
- color: "transparent"
-
- Loader {
- id: leftLauncherLoader
-
- anchors.fill: parent
- anchors.margins: Appearance.padding.large + Appearance.padding.normal
- anchors.leftMargin: Appearance.padding.large
- anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
-
- asynchronous: true
- sourceComponent: leftContentComponent
- }
- }
-
- InnerBorder {
- id: leftLauncherBorder
- leftThickness: 0
- rightThickness: Appearance.padding.normal / 2
- }
+ SplitPaneLayout {
+ anchors.fill: parent
- Component {
- id: leftContentComponent
+ leftContent: Component {
ColumnLayout {
id: leftLauncherLayout
@@ -336,7 +302,8 @@ RowLayout {
// Lazy load: activate when left pane is loaded
// The ListView will load asynchronously, and search will work because filteredApps
// is updated regardless of whether the ListView is loaded
- return leftLauncherLoader.item !== null;
+ // Access loader through parent - this will be set when component loads
+ return true;
}
sourceComponent: StyledListView {
@@ -412,25 +379,10 @@ RowLayout {
}
}
}
- }
-
- Item {
- id: rightLauncherItem
- Layout.fillWidth: true
- Layout.fillHeight: true
-
- ClippingRectangle {
- id: rightLauncherClippingRect
- anchors.fill: parent
- anchors.margins: Appearance.padding.normal
- anchors.leftMargin: 0
- anchors.rightMargin: Appearance.padding.normal / 2
- radius: rightLauncherBorder.innerRadius
- color: "transparent"
-
- Loader {
- id: rightLauncherLoader
+ rightContent: Component {
+ Item {
+ id: rightLauncherPane
property var pane: root.session.launcher.active
property string paneId: pane ? (pane.id || pane.entry?.id || "") : ""
@@ -442,28 +394,34 @@ RowLayout {
return pane ? appDetails : settings;
}
- anchors.fill: parent
- anchors.margins: Appearance.padding.large * 2
-
- opacity: 1
- scale: 1
- transformOrigin: Item.Center
- clip: false
-
- asynchronous: true
- sourceComponent: rightLauncherLoader.targetComponent
- active: true
-
Component.onCompleted: {
displayedApp = pane;
targetComponent = getComponentForPane();
nextComponent = targetComponent;
}
- onItemChanged: {
- // Ensure displayedApp is set when item is created (for async loading)
- if (item && pane && displayedApp !== pane) {
- displayedApp = pane;
+ Loader {
+ id: rightLauncherLoader
+
+ anchors.fill: parent
+
+ opacity: 1
+ scale: 1
+ transformOrigin: Item.Center
+ clip: false
+
+ asynchronous: true
+ sourceComponent: rightLauncherPane.targetComponent
+ active: true
+
+ // Expose displayedApp to loaded components
+ property var displayedApp: rightLauncherPane.displayedApp
+
+ onItemChanged: {
+ // Ensure displayedApp is set when item is created (for async loading)
+ if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) {
+ rightLauncherPane.displayedApp = rightLauncherPane.pane;
+ }
}
}
@@ -484,9 +442,9 @@ RowLayout {
}
}
PropertyAction {
- target: rightLauncherLoader
+ target: rightLauncherPane
property: "displayedApp"
- value: rightLauncherLoader.pane
+ value: rightLauncherPane.pane
}
PropertyAction {
target: rightLauncherLoader
@@ -494,9 +452,9 @@ RowLayout {
value: false
}
PropertyAction {
- target: rightLauncherLoader
+ target: rightLauncherPane
property: "targetComponent"
- value: rightLauncherLoader.nextComponent
+ value: rightLauncherPane.nextComponent
}
PropertyAction {
target: rightLauncherLoader
@@ -539,90 +497,90 @@ RowLayout {
}
}
}
+ }
- InnerBorder {
- id: rightLauncherBorder
-
- leftThickness: Appearance.padding.normal / 2
- }
-
- Component {
- id: settings
+ Component {
+ id: settings
- StyledFlickable {
- id: settingsFlickable
- flickableDirection: Flickable.VerticalFlick
- contentHeight: settingsInner.height
+ StyledFlickable {
+ id: settingsFlickable
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: settingsInner.height
- StyledScrollBar.vertical: StyledScrollBar {
- flickable: settingsFlickable
- }
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: settingsFlickable
+ }
- Settings {
- id: settingsInner
+ Settings {
+ id: settingsInner
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.top: parent.top
- session: root.session
- }
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ session: root.session
}
}
+ }
- Component {
- id: appDetails
+ Component {
+ id: appDetails
- ColumnLayout {
- anchors.fill: parent
+ ColumnLayout {
+ id: appDetailsLayout
+ anchors.fill: parent
- spacing: Appearance.spacing.normal
+ // Get displayedApp from parent Loader (the Loader has displayedApp property we set)
+ readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null
- Item {
- Layout.alignment: Qt.AlignHCenter
- Layout.leftMargin: Appearance.padding.large * 2
- Layout.rightMargin: Appearance.padding.large * 2
- Layout.topMargin: Appearance.padding.large * 2
- implicitWidth: iconLoader.implicitWidth
- implicitHeight: iconLoader.implicitHeight
+ spacing: Appearance.spacing.normal
- Loader {
- id: iconLoader
- sourceComponent: rightLauncherLoader.displayedApp ? appIconComponent : defaultIconComponent
- }
+ Item {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.leftMargin: Appearance.padding.large * 2
+ Layout.rightMargin: Appearance.padding.large * 2
+ Layout.topMargin: Appearance.padding.large * 2
+ implicitWidth: iconLoader.implicitWidth
+ implicitHeight: iconLoader.implicitHeight
- Component {
- id: appIconComponent
- IconImage {
- implicitSize: Appearance.font.size.extraLarge * 3 * 2
- source: {
- if (!rightLauncherLoader.displayedApp) return "image-missing";
- const entry = rightLauncherLoader.displayedApp.entry;
- if (entry && entry.icon) {
- return Quickshell.iconPath(entry.icon, "image-missing");
- }
- return "image-missing";
+ Loader {
+ id: iconLoader
+ sourceComponent: parent.parent.displayedApp ? appIconComponent : defaultIconComponent
+ }
+
+ Component {
+ id: appIconComponent
+ IconImage {
+ implicitSize: Appearance.font.size.extraLarge * 3 * 2
+ source: {
+ const app = iconLoader.parent.parent.displayedApp;
+ if (!app) return "image-missing";
+ const entry = app.entry;
+ if (entry && entry.icon) {
+ return Quickshell.iconPath(entry.icon, "image-missing");
}
+ return "image-missing";
}
}
+ }
- Component {
- id: defaultIconComponent
- MaterialIcon {
- text: "apps"
- font.pointSize: Appearance.font.size.extraLarge * 3
- font.bold: true
- }
+ Component {
+ id: defaultIconComponent
+ MaterialIcon {
+ text: "apps"
+ font.pointSize: Appearance.font.size.extraLarge * 3
+ font.bold: true
}
}
+ }
- StyledText {
- Layout.alignment: Qt.AlignHCenter
- Layout.leftMargin: Appearance.padding.large * 2
- Layout.rightMargin: Appearance.padding.large * 2
- text: rightLauncherLoader.displayedApp ? (rightLauncherLoader.displayedApp.name || rightLauncherLoader.displayedApp.entry?.name || qsTr("Application Details")) : qsTr("Launcher Applications")
- font.pointSize: Appearance.font.size.large
- font.bold: true
- }
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.leftMargin: Appearance.padding.large * 2
+ Layout.rightMargin: Appearance.padding.large * 2
+ text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : qsTr("Launcher Applications")
+ font.pointSize: Appearance.font.size.large
+ font.bold: true
+ }
Item {
Layout.fillWidth: true
@@ -648,38 +606,38 @@ RowLayout {
anchors.top: parent.top
spacing: Appearance.spacing.normal
- SwitchRow {
- Layout.topMargin: Appearance.spacing.normal
- visible: rightLauncherLoader.displayedApp !== null
- label: qsTr("Hide from launcher")
- checked: root.hideFromLauncherChecked
- enabled: rightLauncherLoader.displayedApp !== null
- onToggled: checked => {
- root.hideFromLauncherChecked = checked;
- if (rightLauncherLoader.displayedApp) {
- const appId = rightLauncherLoader.displayedApp.id || rightLauncherLoader.displayedApp.entry?.id;
- const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];
- if (checked) {
- if (!hiddenApps.includes(appId)) {
- hiddenApps.push(appId);
- }
- } else {
- const index = hiddenApps.indexOf(appId);
- if (index !== -1) {
- hiddenApps.splice(index, 1);
- }
+ SwitchRow {
+ Layout.topMargin: Appearance.spacing.normal
+ visible: appDetailsLayout.displayedApp !== null
+ label: qsTr("Hide from launcher")
+ checked: root.hideFromLauncherChecked
+ enabled: appDetailsLayout.displayedApp !== null
+ onToggled: checked => {
+ root.hideFromLauncherChecked = checked;
+ const app = appDetailsLayout.displayedApp;
+ if (app) {
+ const appId = app.id || app.entry?.id;
+ const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];
+ if (checked) {
+ if (!hiddenApps.includes(appId)) {
+ hiddenApps.push(appId);
+ }
+ } else {
+ const index = hiddenApps.indexOf(appId);
+ if (index !== -1) {
+ hiddenApps.splice(index, 1);
}
- Config.launcher.hiddenApps = hiddenApps;
- Config.save();
}
+ Config.launcher.hiddenApps = hiddenApps;
+ Config.save();
}
}
+ }
}
}
}
}
- }
}
component Anim: NumberAnimation {
diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml
index d0ea852..55c70d2 100644
--- a/modules/controlcenter/network/NetworkingPane.qml
+++ b/modules/controlcenter/network/NetworkingPane.qml
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import ".."
+import "../components"
import "."
import qs.components
import qs.components.controls
@@ -21,49 +22,12 @@ Item {
anchors.fill: parent
- RowLayout {
- id: contentLayout
+ SplitPaneLayout {
+ id: splitLayout
anchors.fill: parent
- spacing: 0
- Item {
- id: leftNetworkItem
- Layout.preferredWidth: Math.floor(parent.width * 0.4)
- Layout.minimumWidth: 420
- Layout.fillHeight: true
-
- ClippingRectangle {
- id: leftNetworkClippingRect
- anchors.fill: parent
- anchors.margins: Appearance.padding.normal
- anchors.leftMargin: 0
- anchors.rightMargin: Appearance.padding.normal / 2
-
- radius: leftNetworkBorder.innerRadius
- color: "transparent"
-
- Loader {
- id: leftNetworkLoader
-
- anchors.fill: parent
- anchors.margins: Appearance.padding.large + Appearance.padding.normal
- anchors.leftMargin: Appearance.padding.large
- anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
-
- asynchronous: true
- sourceComponent: networkListComponent
- }
- }
-
- InnerBorder {
- id: leftNetworkBorder
- leftThickness: 0
- rightThickness: Appearance.padding.normal / 2
- }
-
- Component {
- id: networkListComponent
+ leftContent: Component {
StyledFlickable {
id: leftFlickable
@@ -473,38 +437,47 @@ Item {
}
}
}
- }
}
- Item {
- id: rightNetworkItem
- Layout.fillWidth: true
- Layout.fillHeight: true
-
- ClippingRectangle {
- id: networkClippingRect
- anchors.fill: parent
- anchors.margins: Appearance.padding.normal
- anchors.leftMargin: 0
- anchors.rightMargin: Appearance.padding.normal / 2
+ rightContent: Component {
+ Item {
+ id: rightPaneItem
+
+ // Right pane - networking details/settings
+ property var ethernetPane: root.session.ethernet.active
+ property var wirelessPane: root.session.network.active
+ property var pane: ethernetPane || wirelessPane
+ property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "")
+ property Component targetComponent: settings
+ property Component nextComponent: settings
- radius: rightBorder.innerRadius
- color: "transparent"
+ function getComponentForPane() {
+ return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings;
+ }
- // Right pane - networking details/settings
- Loader {
- id: loader
+ Component.onCompleted: {
+ targetComponent = getComponentForPane();
+ nextComponent = targetComponent;
+ }
- property var ethernetPane: root.session.ethernet.active
- property var wirelessPane: root.session.network.active
- property var pane: ethernetPane || wirelessPane
- property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "")
- property Component targetComponent: settings
- property Component nextComponent: settings
+ Connections {
+ target: root.session.ethernet
+ function onActiveChanged() {
+ nextComponent = getComponentForPane();
+ paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "");
+ }
+ }
- function getComponentForPane() {
- return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings;
+ Connections {
+ target: root.session.network
+ function onActiveChanged() {
+ nextComponent = getComponentForPane();
+ paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "");
}
+ }
+
+ Loader {
+ id: rightLoader
anchors.fill: parent
anchors.margins: Appearance.padding.large * 2
@@ -515,131 +488,129 @@ Item {
clip: false
asynchronous: true
- sourceComponent: loader.targetComponent
+ sourceComponent: rightPaneItem.targetComponent
- Component.onCompleted: {
- targetComponent = getComponentForPane();
- nextComponent = targetComponent;
+ Connections {
+ target: rightPaneItem
+ function onPaneIdChanged() {
+ rightPaneItem.targetComponent = rightPaneItem.nextComponent;
+ }
}
+ }
- Behavior on paneId {
- SequentialAnimation {
- ParallelAnimation {
- Anim {
- target: loader
- property: "opacity"
- to: 0
- easing.bezierCurve: Appearance.anim.curves.standardAccel
- }
- Anim {
- target: loader
- property: "scale"
- to: 0.8
- easing.bezierCurve: Appearance.anim.curves.standardAccel
- }
+ Behavior on paneId {
+ SequentialAnimation {
+ ParallelAnimation {
+ Anim {
+ target: rightLoader
+ property: "opacity"
+ to: 0
+ easing.bezierCurve: Appearance.anim.curves.standardAccel
}
- PropertyAction {
- target: loader
- property: "targetComponent"
- value: loader.nextComponent
+ Anim {
+ target: rightLoader
+ property: "scale"
+ to: 0.8
+ easing.bezierCurve: Appearance.anim.curves.standardAccel
}
- ParallelAnimation {
- Anim {
- target: loader
- property: "opacity"
- to: 1
- easing.bezierCurve: Appearance.anim.curves.standardDecel
- }
- Anim {
- target: loader
- property: "scale"
- to: 1
- easing.bezierCurve: Appearance.anim.curves.standardDecel
- }
+ }
+ PropertyAction {
+ target: rightPaneItem
+ property: "targetComponent"
+ value: rightPaneItem.nextComponent
+ }
+ ParallelAnimation {
+ Anim {
+ target: rightLoader
+ property: "opacity"
+ to: 1
+ easing.bezierCurve: Appearance.anim.curves.standardDecel
+ }
+ Anim {
+ target: rightLoader
+ property: "scale"
+ to: 1
+ easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
}
+ }
- onPaneChanged: {
- nextComponent = getComponentForPane();
- paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "");
+ Connections {
+ target: rightPaneItem
+ function onPaneIdChanged() {
+ rightPaneItem.targetComponent = rightPaneItem.nextComponent;
}
}
}
+ }
+ }
- InnerBorder {
- id: rightBorder
-
- leftThickness: Appearance.padding.normal / 2
- }
-
- Component {
- id: settings
+ Component {
+ id: settings
- StyledFlickable {
- id: settingsFlickable
- flickableDirection: Flickable.VerticalFlick
- contentHeight: settingsInner.height
+ StyledFlickable {
+ id: settingsFlickable
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: settingsInner.height
- StyledScrollBar.vertical: StyledScrollBar {
- flickable: settingsFlickable
- }
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: settingsFlickable
+ }
- NetworkSettings {
- id: settingsInner
+ NetworkSettings {
+ id: settingsInner
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.top: parent.top
- session: root.session
- }
- }
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ session: root.session
}
+ }
+ }
- Component {
- id: ethernetDetails
+ Component {
+ id: ethernetDetails
- StyledFlickable {
- id: ethernetFlickable
- flickableDirection: Flickable.VerticalFlick
- contentHeight: ethernetDetailsInner.height
+ StyledFlickable {
+ id: ethernetFlickable
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: ethernetDetailsInner.height
- StyledScrollBar.vertical: StyledScrollBar {
- flickable: ethernetFlickable
- }
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: ethernetFlickable
+ }
- EthernetDetails {
- id: ethernetDetailsInner
+ EthernetDetails {
+ id: ethernetDetailsInner
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.top: parent.top
- session: root.session
- }
- }
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ session: root.session
}
+ }
+ }
- Component {
- id: wirelessDetails
+ Component {
+ id: wirelessDetails
- StyledFlickable {
- id: wirelessFlickable
- flickableDirection: Flickable.VerticalFlick
- contentHeight: wirelessDetailsInner.height
+ StyledFlickable {
+ id: wirelessFlickable
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: wirelessDetailsInner.height
- StyledScrollBar.vertical: StyledScrollBar {
- flickable: wirelessFlickable
- }
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: wirelessFlickable
+ }
- WirelessDetails {
- id: wirelessDetailsInner
+ WirelessDetails {
+ id: wirelessDetailsInner
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.top: parent.top
- session: root.session
- }
- }
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ session: root.session
}
}
}
@@ -651,7 +622,6 @@ Item {
}
component Anim: NumberAnimation {
- target: loader
duration: Appearance.anim.durations.normal / 2
easing.type: Easing.BezierSpline
}