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
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
}
}
StyledFlickable {
id: rightAudioFlickable
anchors.fill: parent
flickableDirection: Flickable.VerticalFlick
contentHeight: contentLayout.height
InnerBorder {
id: rightAudioBorder
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
StyledScrollBar.vertical: StyledScrollBar {
flickable: rightAudioFlickable
}
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
id: contentLayout
RowLayout {
Layout.fillWidth: true
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
StyledText {
text: qsTr("Volume")
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
ConnectionHeader {
icon: "volume_up"
title: qsTr("Audio Settings")
}
Item {
SectionHeader {
title: qsTr("Output volume")
description: qsTr("Control the volume of your output device")
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
}
spacing: Appearance.spacing.small
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
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
StyledText {
text: qsTr("Volume")
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
MouseArea {
id: outputVolumeInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: outputVolumeInput.implicitHeight + Appearance.padding.small * 2
color: outputVolumeInputHover.containsMouse || outputVolumeInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: outputVolumeInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
enabled: !Audio.muted
opacity: enabled ? 1 : 0.5
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: outputVolumeInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
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 {
id: outputVolumeInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: IntValidator { bottom: 0; top: 100 }
StyledSlider {
id: outputVolumeSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
value: Audio.volume
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();
}
opacity: enabled ? 1 : 0.5
onMoved: {
Audio.setVolume(value);
if (!outputVolumeInput.activeFocus) {
outputVolumeInput.text = Math.round(value * 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 {
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
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Volume")
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
Item {
ColumnLayout {
Layout.fillWidth: true
}
spacing: Appearance.spacing.small
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
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
StyledText {
text: qsTr("Volume")
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
MouseArea {
id: inputVolumeInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: inputVolumeInput.implicitHeight + Appearance.padding.small * 2
color: inputVolumeInputHover.containsMouse || inputVolumeInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: inputVolumeInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
enabled: !Audio.sourceMuted
opacity: enabled ? 1 : 0.5
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: inputVolumeInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
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 {
id: inputVolumeInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: IntValidator { bottom: 0; top: 100 }
StyledSlider {
id: inputVolumeSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
value: Audio.sourceVolume
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();
}
opacity: enabled ? 1 : 0.5
onMoved: {
Audio.setSourceVolume(value);
if (!inputVolumeInput.activeFocus) {
inputVolumeInput.text = Math.round(value * 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
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
}
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
Connections {
target: root.session.bt
function onActiveChanged() {
rightBtPane.pane = root.session.bt.active;
}
}
}
}
}
Component {
id: details
Component {
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
}
}
}
Component {
id: details
Details {
session: root.session
}
}
component Anim: NumberAnimation {
target: loader
duration: Appearance.anim.durations.normal / 2
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
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
SplitPaneLayout {
anchors.fill: parent
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
}
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
Component {
id: settings
leftThickness: Appearance.padding.normal / 2
}
StyledFlickable {
id: settingsFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: settingsInner.height
Component {
id: settings
StyledScrollBar.vertical: StyledScrollBar {
flickable: settingsFlickable
}
StyledFlickable {
id: settingsFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: settingsInner.height
Settings {
id: settingsInner
StyledScrollBar.vertical: StyledScrollBar {
flickable: settingsFlickable
}
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");
}
}
}
Component {
id: defaultIconComponent
MaterialIcon {
text: "apps"
font.pointSize: Appearance.font.size.extraLarge * 3
font.bold: true
return "image-missing";
}
}
}
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
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: 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 {

View file

@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import ".."
import "../components"
import "."
import qs.components
import qs.components.controls
@ -21,49 +22,12 @@ Item {
anchors.fill: parent
RowLayout {
id: contentLayout
SplitPaneLayout {
id: splitLayout
anchors.fill: parent
spacing: 0
Item {
id: leftNetworkItem
Layout.preferredWidth: Math.floor(parent.width * 0.4)
Layout.minimumWidth: 420
Layout.fillHeight: true
ClippingRectangle {
id: leftNetworkClippingRect
anchors.fill: parent
anchors.margins: Appearance.padding.normal
anchors.leftMargin: 0
anchors.rightMargin: Appearance.padding.normal / 2
radius: leftNetworkBorder.innerRadius
color: "transparent"
Loader {
id: leftNetworkLoader
anchors.fill: parent
anchors.margins: Appearance.padding.large + Appearance.padding.normal
anchors.leftMargin: Appearance.padding.large
anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
asynchronous: true
sourceComponent: networkListComponent
}
}
InnerBorder {
id: leftNetworkBorder
leftThickness: 0
rightThickness: Appearance.padding.normal / 2
}
Component {
id: networkListComponent
leftContent: Component {
StyledFlickable {
id: leftFlickable
@ -473,38 +437,47 @@ Item {
}
}
}
}
}
Item {
id: rightNetworkItem
Layout.fillWidth: true
Layout.fillHeight: true
ClippingRectangle {
id: networkClippingRect
anchors.fill: parent
anchors.margins: Appearance.padding.normal
anchors.leftMargin: 0
anchors.rightMargin: Appearance.padding.normal / 2
radius: rightBorder.innerRadius
color: "transparent"
rightContent: Component {
Item {
id: rightPaneItem
// Right pane - networking details/settings
Loader {
id: loader
property var ethernetPane: root.session.ethernet.active
property var wirelessPane: root.session.network.active
property var pane: ethernetPane || wirelessPane
property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "")
property Component targetComponent: settings
property Component nextComponent: settings
property var ethernetPane: root.session.ethernet.active
property var wirelessPane: root.session.network.active
property var pane: ethernetPane || wirelessPane
property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "")
property Component targetComponent: settings
property Component nextComponent: settings
function getComponentForPane() {
return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings;
}
function getComponentForPane() {
return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings;
Component.onCompleted: {
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.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
Component {
id: settings
leftThickness: Appearance.padding.normal / 2
StyledFlickable {
id: settingsFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: settingsInner.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: settingsFlickable
}
Component {
id: settings
NetworkSettings {
id: settingsInner
StyledFlickable {
id: settingsFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: settingsInner.height
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
session: root.session
}
}
}
StyledScrollBar.vertical: StyledScrollBar {
flickable: settingsFlickable
}
Component {
id: ethernetDetails
NetworkSettings {
id: settingsInner
StyledFlickable {
id: ethernetFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: ethernetDetailsInner.height
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
session: root.session
}
}
StyledScrollBar.vertical: StyledScrollBar {
flickable: ethernetFlickable
}
Component {
id: ethernetDetails
EthernetDetails {
id: ethernetDetailsInner
StyledFlickable {
id: ethernetFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: ethernetDetailsInner.height
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
session: root.session
}
}
}
StyledScrollBar.vertical: StyledScrollBar {
flickable: ethernetFlickable
}
Component {
id: wirelessDetails
EthernetDetails {
id: ethernetDetailsInner
StyledFlickable {
id: wirelessFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: wirelessDetailsInner.height
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
session: root.session
}
}
StyledScrollBar.vertical: StyledScrollBar {
flickable: wirelessFlickable
}
Component {
id: wirelessDetails
WirelessDetails {
id: wirelessDetailsInner
StyledFlickable {
id: wirelessFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: wirelessDetailsInner.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: wirelessFlickable
}
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
}