refactor: SplitPaneLayout now component

This commit is contained in:
ATMDA 2025-11-19 12:39:45 -05:00
parent 50e29f8be3
commit ad4213d45c
6 changed files with 1317 additions and 1436 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import ".." import ".."
import "../components"
import qs.components import qs.components
import qs.components.controls import qs.components.controls
import qs.components.effects import qs.components.effects
@ -11,52 +12,17 @@ import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
RowLayout { Item {
id: root id: root
required property Session session required property Session session
anchors.fill: parent anchors.fill: parent
spacing: 0 SplitPaneLayout {
anchors.fill: parent
Item { leftContent: Component {
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
StyledFlickable { StyledFlickable {
id: leftAudioFlickable 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.fill: parent
anchors.topMargin: Appearance.padding.large * 2 anchors.topMargin: Appearance.padding.large * 2
anchors.bottomMargin: Appearance.padding.large * 2 anchors.bottomMargin: Appearance.padding.large * 2
anchors.leftMargin: 0 anchors.leftMargin: 0
anchors.rightMargin: 0 anchors.rightMargin: 0
asynchronous: true StyledFlickable {
sourceComponent: audioRightContentComponent id: rightAudioFlickable
} anchors.fill: parent
} flickableDirection: Flickable.VerticalFlick
contentHeight: contentLayout.height
InnerBorder { StyledScrollBar.vertical: StyledScrollBar {
id: rightAudioBorder flickable: rightAudioFlickable
leftThickness: Appearance.padding.normal / 2 }
}
Component {
id: audioRightContentComponent
StyledFlickable {
id: rightAudioFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: contentLayout.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: rightAudioFlickable
}
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
ConnectionHeader {
icon: "volume_up"
title: qsTr("Audio Settings")
}
SectionHeader {
title: qsTr("Output volume")
description: qsTr("Control the volume of your output device")
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout { ColumnLayout {
Layout.fillWidth: true id: contentLayout
spacing: Appearance.spacing.small
RowLayout { anchors.left: parent.left
Layout.fillWidth: true anchors.right: parent.right
spacing: Appearance.spacing.normal anchors.leftMargin: Appearance.padding.large * 2
anchors.rightMargin: Appearance.padding.large * 2
spacing: Appearance.spacing.normal
StyledText { ConnectionHeader {
text: qsTr("Volume") icon: "volume_up"
font.pointSize: Appearance.font.size.normal title: qsTr("Audio Settings")
font.weight: 500 }
}
Item { SectionHeader {
title: qsTr("Output volume")
description: qsTr("Control the volume of your output device")
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
} spacing: Appearance.spacing.small
StyledRect { RowLayout {
Layout.preferredWidth: 70 Layout.fillWidth: true
implicitHeight: outputVolumeInput.implicitHeight + Appearance.padding.small * 2 spacing: Appearance.spacing.normal
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 {} } StyledText {
Behavior on border.color { CAnim {} } text: qsTr("Volume")
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
MouseArea { Item {
id: outputVolumeInputHover Layout.fillWidth: true
anchors.fill: parent }
hoverEnabled: true
cursorShape: Qt.IBeamCursor StyledRect {
acceptedButtons: Qt.NoButton 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();
}
}
}
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();
}
}
}
}
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
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;
}
}
}
MaterialIcon {
id: muteIcon
anchors.centerIn: parent
text: Audio.muted ? "volume_off" : "volume_up"
color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
}
}
} }
StyledTextField { StyledSlider {
id: outputVolumeInput id: outputVolumeSlider
anchors.centerIn: parent Layout.fillWidth: true
width: parent.width - Appearance.padding.normal implicitHeight: Appearance.padding.normal * 3
horizontalAlignment: TextInput.AlignHCenter
validator: IntValidator { bottom: 0; top: 100 } value: Audio.volume
enabled: !Audio.muted enabled: !Audio.muted
opacity: enabled ? 1 : 0.5
Component.onCompleted: { onMoved: {
text = Math.round(Audio.volume * 100).toString(); Audio.setVolume(value);
} if (!outputVolumeInput.activeFocus) {
outputVolumeInput.text = Math.round(value * 100).toString();
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();
}
}
}
}
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
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;
}
}
}
MaterialIcon {
id: muteIcon
anchors.centerIn: parent
text: Audio.muted ? "volume_off" : "volume_up"
color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
} }
} }
} }
StyledSlider { SectionHeader {
id: outputVolumeSlider title: qsTr("Input volume")
Layout.fillWidth: true description: qsTr("Control the volume of your input device")
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 { SectionContainer {
title: qsTr("Input volume") contentSpacing: Appearance.spacing.normal
description: qsTr("Control the volume of your input device")
}
SectionContainer { ColumnLayout {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Volume")
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
Item {
Layout.fillWidth: true Layout.fillWidth: true
} spacing: Appearance.spacing.small
StyledRect { RowLayout {
Layout.preferredWidth: 70 Layout.fillWidth: true
implicitHeight: inputVolumeInput.implicitHeight + Appearance.padding.small * 2 spacing: Appearance.spacing.normal
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 {} } StyledText {
Behavior on border.color { CAnim {} } text: qsTr("Volume")
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
MouseArea { Item {
id: inputVolumeInputHover Layout.fillWidth: true
anchors.fill: parent }
hoverEnabled: true
cursorShape: Qt.IBeamCursor StyledRect {
acceptedButtons: Qt.NoButton 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();
}
}
}
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();
}
}
}
}
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
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;
}
}
}
MaterialIcon {
id: muteInputIcon
anchors.centerIn: parent
text: "mic_off"
color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
}
}
} }
StyledTextField { StyledSlider {
id: inputVolumeInput id: inputVolumeSlider
anchors.centerIn: parent Layout.fillWidth: true
width: parent.width - Appearance.padding.normal implicitHeight: Appearance.padding.normal * 3
horizontalAlignment: TextInput.AlignHCenter
validator: IntValidator { bottom: 0; top: 100 } value: Audio.sourceVolume
enabled: !Audio.sourceMuted enabled: !Audio.sourceMuted
opacity: enabled ? 1 : 0.5
Component.onCompleted: { onMoved: {
text = Math.round(Audio.sourceVolume * 100).toString(); Audio.setSourceVolume(value);
} if (!inputVolumeInput.activeFocus) {
inputVolumeInput.text = Math.round(value * 100).toString();
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();
}
}
}
}
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
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;
}
}
}
MaterialIcon {
id: muteInputIcon
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();
} }
} }
} }
} }
} }
} }
}
} }
} }
} }

View file

@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import ".." import ".."
import "../components"
import qs.components.controls import qs.components.controls
import qs.components.effects import qs.components.effects
import qs.components.containers import qs.components.containers
@ -10,95 +11,50 @@ import Quickshell.Bluetooth
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
RowLayout { Item {
id: root id: root
required property Session session required property Session session
anchors.fill: parent anchors.fill: parent
spacing: 0 SplitPaneLayout {
anchors.fill: parent
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
leftContent: Component {
DeviceList { DeviceList {
anchors.fill: parent anchors.fill: parent
session: root.session session: root.session
} }
} }
}
Item { rightContent: Component {
id: rightBtItem Item {
Layout.fillWidth: true id: rightBtPane
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
property BluetoothDevice pane: root.session.bt.active property BluetoothDevice pane: root.session.bt.active
anchors.fill: parent Loader {
anchors.margins: Appearance.padding.large * 2 id: rightLoader
asynchronous: true anchors.fill: parent
sourceComponent: pane ? details : settings anchors.margins: Appearance.padding.large * 2
asynchronous: true
sourceComponent: rightBtPane.pane ? details : settings
}
Behavior on pane { Behavior on pane {
SequentialAnimation { SequentialAnimation {
ParallelAnimation { ParallelAnimation {
Anim { Anim {
target: rightLoader
property: "opacity" property: "opacity"
to: 0 to: 0
easing.bezierCurve: Appearance.anim.curves.standardAccel easing.bezierCurve: Appearance.anim.curves.standardAccel
} }
Anim { Anim {
target: rightLoader
property: "scale" property: "scale"
to: 0.8 to: 0.8
easing.bezierCurve: Appearance.anim.curves.standardAccel easing.bezierCurve: Appearance.anim.curves.standardAccel
@ -107,11 +63,13 @@ RowLayout {
PropertyAction {} PropertyAction {}
ParallelAnimation { ParallelAnimation {
Anim { Anim {
target: rightLoader
property: "opacity" property: "opacity"
to: 1 to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel easing.bezierCurve: Appearance.anim.curves.standardDecel
} }
Anim { Anim {
target: rightLoader
property: "scale" property: "scale"
to: 1 to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel easing.bezierCurve: Appearance.anim.curves.standardDecel
@ -119,49 +77,49 @@ RowLayout {
} }
} }
} }
}
}
InnerBorder { Connections {
id: rightBorder target: root.session.bt
function onActiveChanged() {
leftThickness: Appearance.padding.normal / 2 rightBtPane.pane = root.session.bt.active;
} }
Component {
id: settings
StyledFlickable {
id: settingsFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: settingsInner.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: settingsFlickable
}
Settings {
id: settingsInner
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
session: root.session
} }
} }
} }
}
Component { Component {
id: details id: settings
Details { StyledFlickable {
id: settingsFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: settingsInner.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: settingsFlickable
}
Settings {
id: settingsInner
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
session: root.session session: root.session
} }
} }
} }
Component {
id: details
Details {
session: root.session
}
}
component Anim: NumberAnimation { component Anim: NumberAnimation {
target: loader
duration: Appearance.anim.durations.normal / 2 duration: Appearance.anim.durations.normal / 2
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
} }

View file

@ -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
}
}
}

View file

@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import ".." import ".."
import "../components"
import "../../launcher/services" import "../../launcher/services"
import qs.components import qs.components
import qs.components.controls import qs.components.controls
@ -16,7 +17,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import "../../../utils/scripts/fuzzysort.js" as Fuzzy import "../../../utils/scripts/fuzzysort.js" as Fuzzy
RowLayout { Item {
id: root id: root
required property Session session required property Session session
@ -26,8 +27,6 @@ RowLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0
onSelectedAppChanged: { onSelectedAppChanged: {
root.session.launcher.active = root.selectedApp; root.session.launcher.active = root.selectedApp;
updateToggleState(); updateToggleState();
@ -156,43 +155,10 @@ RowLayout {
} }
} }
Item { SplitPaneLayout {
id: leftLauncherItem anchors.fill: parent
Layout.preferredWidth: Math.floor(parent.width * 0.4)
Layout.minimumWidth: 420
Layout.fillHeight: true
ClippingRectangle { leftContent: Component {
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
}
Component {
id: leftContentComponent
ColumnLayout { ColumnLayout {
id: leftLauncherLayout id: leftLauncherLayout
@ -336,7 +302,8 @@ RowLayout {
// Lazy load: activate when left pane is loaded // Lazy load: activate when left pane is loaded
// The ListView will load asynchronously, and search will work because filteredApps // The ListView will load asynchronously, and search will work because filteredApps
// is updated regardless of whether the ListView is loaded // 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 { sourceComponent: StyledListView {
@ -412,25 +379,10 @@ RowLayout {
} }
} }
} }
}
Item { rightContent: Component {
id: rightLauncherItem Item {
Layout.fillWidth: true id: rightLauncherPane
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
property var pane: root.session.launcher.active property var pane: root.session.launcher.active
property string paneId: pane ? (pane.id || pane.entry?.id || "") : "" property string paneId: pane ? (pane.id || pane.entry?.id || "") : ""
@ -442,28 +394,34 @@ RowLayout {
return pane ? appDetails : settings; 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: { Component.onCompleted: {
displayedApp = pane; displayedApp = pane;
targetComponent = getComponentForPane(); targetComponent = getComponentForPane();
nextComponent = targetComponent; nextComponent = targetComponent;
} }
onItemChanged: { Loader {
// Ensure displayedApp is set when item is created (for async loading) id: rightLauncherLoader
if (item && pane && displayedApp !== pane) {
displayedApp = pane; 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 { PropertyAction {
target: rightLauncherLoader target: rightLauncherPane
property: "displayedApp" property: "displayedApp"
value: rightLauncherLoader.pane value: rightLauncherPane.pane
} }
PropertyAction { PropertyAction {
target: rightLauncherLoader target: rightLauncherLoader
@ -494,9 +452,9 @@ RowLayout {
value: false value: false
} }
PropertyAction { PropertyAction {
target: rightLauncherLoader target: rightLauncherPane
property: "targetComponent" property: "targetComponent"
value: rightLauncherLoader.nextComponent value: rightLauncherPane.nextComponent
} }
PropertyAction { PropertyAction {
target: rightLauncherLoader target: rightLauncherLoader
@ -539,90 +497,90 @@ RowLayout {
} }
} }
} }
}
InnerBorder { Component {
id: rightLauncherBorder id: settings
leftThickness: Appearance.padding.normal / 2 StyledFlickable {
} id: settingsFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: settingsInner.height
Component { StyledScrollBar.vertical: StyledScrollBar {
id: settings flickable: settingsFlickable
}
StyledFlickable { Settings {
id: settingsFlickable id: settingsInner
flickableDirection: Flickable.VerticalFlick
contentHeight: settingsInner.height
StyledScrollBar.vertical: StyledScrollBar { anchors.left: parent.left
flickable: settingsFlickable anchors.right: parent.right
} anchors.top: parent.top
session: root.session
Settings {
id: settingsInner
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
session: root.session
}
} }
} }
}
Component { Component {
id: appDetails id: appDetails
ColumnLayout { ColumnLayout {
anchors.fill: parent 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 { spacing: Appearance.spacing.normal
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
Loader { Item {
id: iconLoader Layout.alignment: Qt.AlignHCenter
sourceComponent: rightLauncherLoader.displayedApp ? appIconComponent : defaultIconComponent 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 { Loader {
id: appIconComponent id: iconLoader
IconImage { sourceComponent: parent.parent.displayedApp ? appIconComponent : defaultIconComponent
implicitSize: Appearance.font.size.extraLarge * 3 * 2 }
source: {
if (!rightLauncherLoader.displayedApp) return "image-missing"; Component {
const entry = rightLauncherLoader.displayedApp.entry; id: appIconComponent
if (entry && entry.icon) { IconImage {
return Quickshell.iconPath(entry.icon, "image-missing"); implicitSize: Appearance.font.size.extraLarge * 3 * 2
} source: {
return "image-missing"; 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
} }
} }
} }
StyledText { Component {
Layout.alignment: Qt.AlignHCenter id: defaultIconComponent
Layout.leftMargin: Appearance.padding.large * 2 MaterialIcon {
Layout.rightMargin: Appearance.padding.large * 2 text: "apps"
text: rightLauncherLoader.displayedApp ? (rightLauncherLoader.displayedApp.name || rightLauncherLoader.displayedApp.entry?.name || qsTr("Application Details")) : qsTr("Launcher Applications") font.pointSize: Appearance.font.size.extraLarge * 3
font.pointSize: Appearance.font.size.large font.bold: true
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 { Item {
Layout.fillWidth: true Layout.fillWidth: true
@ -648,38 +606,38 @@ RowLayout {
anchors.top: parent.top anchors.top: parent.top
spacing: Appearance.spacing.normal spacing: Appearance.spacing.normal
SwitchRow { SwitchRow {
Layout.topMargin: Appearance.spacing.normal Layout.topMargin: Appearance.spacing.normal
visible: rightLauncherLoader.displayedApp !== null visible: appDetailsLayout.displayedApp !== null
label: qsTr("Hide from launcher") label: qsTr("Hide from launcher")
checked: root.hideFromLauncherChecked checked: root.hideFromLauncherChecked
enabled: rightLauncherLoader.displayedApp !== null enabled: appDetailsLayout.displayedApp !== null
onToggled: checked => { onToggled: checked => {
root.hideFromLauncherChecked = checked; root.hideFromLauncherChecked = checked;
if (rightLauncherLoader.displayedApp) { const app = appDetailsLayout.displayedApp;
const appId = rightLauncherLoader.displayedApp.id || rightLauncherLoader.displayedApp.entry?.id; if (app) {
const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; const appId = app.id || app.entry?.id;
if (checked) { const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];
if (!hiddenApps.includes(appId)) { if (checked) {
hiddenApps.push(appId); if (!hiddenApps.includes(appId)) {
} hiddenApps.push(appId);
} else { }
const index = hiddenApps.indexOf(appId); } else {
if (index !== -1) { const index = hiddenApps.indexOf(appId);
hiddenApps.splice(index, 1); if (index !== -1) {
} hiddenApps.splice(index, 1);
} }
Config.launcher.hiddenApps = hiddenApps;
Config.save();
} }
Config.launcher.hiddenApps = hiddenApps;
Config.save();
} }
} }
}
} }
} }
} }
} }
}
} }
component Anim: NumberAnimation { component Anim: NumberAnimation {

View file

@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import ".." import ".."
import "../components"
import "." import "."
import qs.components import qs.components
import qs.components.controls import qs.components.controls
@ -21,49 +22,12 @@ Item {
anchors.fill: parent anchors.fill: parent
RowLayout { SplitPaneLayout {
id: contentLayout id: splitLayout
anchors.fill: parent anchors.fill: parent
spacing: 0
Item { leftContent: Component {
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
StyledFlickable { StyledFlickable {
id: leftFlickable id: leftFlickable
@ -473,38 +437,47 @@ Item {
} }
} }
} }
}
} }
Item { rightContent: Component {
id: rightNetworkItem Item {
Layout.fillWidth: true id: rightPaneItem
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"
// Right pane - networking details/settings // Right pane - networking details/settings
Loader { property var ethernetPane: root.session.ethernet.active
id: loader 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
property var ethernetPane: root.session.ethernet.active function getComponentForPane() {
property var wirelessPane: root.session.network.active return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings;
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() { Component.onCompleted: {
return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings; targetComponent = getComponentForPane();
nextComponent = targetComponent;
}
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.fill: parent
anchors.margins: Appearance.padding.large * 2 anchors.margins: Appearance.padding.large * 2
@ -515,131 +488,129 @@ Item {
clip: false clip: false
asynchronous: true asynchronous: true
sourceComponent: loader.targetComponent sourceComponent: rightPaneItem.targetComponent
Component.onCompleted: { Connections {
targetComponent = getComponentForPane(); target: rightPaneItem
nextComponent = targetComponent; function onPaneIdChanged() {
rightPaneItem.targetComponent = rightPaneItem.nextComponent;
}
} }
}
Behavior on paneId { Behavior on paneId {
SequentialAnimation { SequentialAnimation {
ParallelAnimation { ParallelAnimation {
Anim { Anim {
target: loader target: rightLoader
property: "opacity" property: "opacity"
to: 0 to: 0
easing.bezierCurve: Appearance.anim.curves.standardAccel easing.bezierCurve: Appearance.anim.curves.standardAccel
}
Anim {
target: loader
property: "scale"
to: 0.8
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
} }
PropertyAction { Anim {
target: loader target: rightLoader
property: "targetComponent" property: "scale"
value: loader.nextComponent to: 0.8
easing.bezierCurve: Appearance.anim.curves.standardAccel
} }
ParallelAnimation { }
Anim { PropertyAction {
target: loader target: rightPaneItem
property: "opacity" property: "targetComponent"
to: 1 value: rightPaneItem.nextComponent
easing.bezierCurve: Appearance.anim.curves.standardDecel }
} ParallelAnimation {
Anim { Anim {
target: loader target: rightLoader
property: "scale" property: "opacity"
to: 1 to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel easing.bezierCurve: Appearance.anim.curves.standardDecel
} }
Anim {
target: rightLoader
property: "scale"
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
} }
} }
} }
}
onPaneChanged: { Connections {
nextComponent = getComponentForPane(); target: rightPaneItem
paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); function onPaneIdChanged() {
rightPaneItem.targetComponent = rightPaneItem.nextComponent;
} }
} }
} }
}
}
InnerBorder { Component {
id: rightBorder id: settings
leftThickness: Appearance.padding.normal / 2 StyledFlickable {
id: settingsFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: settingsInner.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: settingsFlickable
} }
Component { NetworkSettings {
id: settings id: settingsInner
StyledFlickable { anchors.left: parent.left
id: settingsFlickable anchors.right: parent.right
flickableDirection: Flickable.VerticalFlick anchors.top: parent.top
contentHeight: settingsInner.height session: root.session
}
}
}
StyledScrollBar.vertical: StyledScrollBar { Component {
flickable: settingsFlickable id: ethernetDetails
}
NetworkSettings { StyledFlickable {
id: settingsInner id: ethernetFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: ethernetDetailsInner.height
anchors.left: parent.left StyledScrollBar.vertical: StyledScrollBar {
anchors.right: parent.right flickable: ethernetFlickable
anchors.top: parent.top
session: root.session
}
}
} }
Component { EthernetDetails {
id: ethernetDetails id: ethernetDetailsInner
StyledFlickable { anchors.left: parent.left
id: ethernetFlickable anchors.right: parent.right
flickableDirection: Flickable.VerticalFlick anchors.top: parent.top
contentHeight: ethernetDetailsInner.height session: root.session
}
}
}
StyledScrollBar.vertical: StyledScrollBar { Component {
flickable: ethernetFlickable id: wirelessDetails
}
EthernetDetails { StyledFlickable {
id: ethernetDetailsInner id: wirelessFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: wirelessDetailsInner.height
anchors.left: parent.left StyledScrollBar.vertical: StyledScrollBar {
anchors.right: parent.right flickable: wirelessFlickable
anchors.top: parent.top
session: root.session
}
}
} }
Component { WirelessDetails {
id: wirelessDetails id: wirelessDetailsInner
StyledFlickable { anchors.left: parent.left
id: wirelessFlickable anchors.right: parent.right
flickableDirection: Flickable.VerticalFlick anchors.top: parent.top
contentHeight: wirelessDetailsInner.height session: root.session
StyledScrollBar.vertical: StyledScrollBar {
flickable: wirelessFlickable
}
WirelessDetails {
id: wirelessDetailsInner
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
session: root.session
}
}
} }
} }
} }
@ -651,7 +622,6 @@ Item {
} }
component Anim: NumberAnimation { component Anim: NumberAnimation {
target: loader
duration: Appearance.anim.durations.normal / 2 duration: Appearance.anim.durations.normal / 2
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
} }