From 5d7151e79e04d8a6073ecb4ea4a14c5c9bdcfc52 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 9 Nov 2025 17:54:16 -0500 Subject: controlcenter: network and audio panels --- modules/BatteryMonitor.qml | 2 +- modules/background/DesktopClock.qml | 1 - modules/controlcenter/NavRail.qml | 2 +- modules/controlcenter/Panes.qml | 22 ++++++---------------- modules/controlcenter/Session.qml | 7 +++++++ modules/controlcenter/WindowFactory.qml | 2 +- modules/controlcenter/WindowTitle.qml | 2 +- services/Network.qml | 21 ++++++++++++++++++--- 8 files changed, 35 insertions(+), 24 deletions(-) diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml index d24cff2..7a3be12 100644 --- a/modules/BatteryMonitor.qml +++ b/modules/BatteryMonitor.qml @@ -15,7 +15,7 @@ Scope { function onOnBatteryChanged(): void { if (UPower.onBattery) { if (Config.utilities.toasts.chargingChanged) - Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off"); + Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is now on AC"), "power_off"); } else { if (Config.utilities.toasts.chargingChanged) Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power"); diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index 2de325c..6dc6b6b 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -12,7 +12,6 @@ Item { anchors.centerIn: parent text: Time.format(Config.services.useTwelveHourClock ? "hh:mm:ss A" : "hh:mm:ss") - font.family: Appearance.font.family.clock font.pointSize: Appearance.font.size.extraLarge font.bold: true } diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 22c13a3..70438a6 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -168,7 +168,7 @@ Item { } NavItem { - icon: "tune" + icon: "volume_up" label: "audio" } } diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 2548c3d..cc4a4cb 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -1,6 +1,8 @@ pragma ComponentBehavior: Bound import "bluetooth" +import "network" +import "audio" import qs.components import qs.services import qs.config @@ -23,14 +25,8 @@ ClippingRectangle { Pane { index: 0 - sourceComponent: Item { - StyledText { - anchors.centerIn: parent - text: qsTr("Work in progress") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.extraLarge - font.weight: 500 - } + sourceComponent: NetworkPane { + session: root.session } } @@ -43,14 +39,8 @@ ClippingRectangle { Pane { index: 2 - sourceComponent: Item { - StyledText { - anchors.centerIn: parent - text: qsTr("Work in progress") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.extraLarge - font.weight: 500 - } + sourceComponent: AudioPane { + session: root.session } } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index a143470..b716b20 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -11,6 +11,7 @@ QtObject { property bool navExpanded: false readonly property Bt bt: Bt {} + readonly property Network network: Network {} onActiveChanged: activeIndex = panes.indexOf(active) onActiveIndexChanged: active = panes[activeIndex] @@ -22,4 +23,10 @@ QtObject { property bool fabMenuOpen property bool editingDeviceName } + + component Network: QtObject { + property var active + property bool showPasswordDialog: false + property var pendingNetwork + } } diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml index c5b7535..14d85da 100644 --- a/modules/controlcenter/WindowFactory.qml +++ b/modules/controlcenter/WindowFactory.qml @@ -38,7 +38,7 @@ Singleton { implicitWidth: cc.implicitWidth implicitHeight: cc.implicitHeight - title: qsTr("Caelestia Settings - %1").arg(cc.active.slice(0, 1).toUpperCase() + cc.active.slice(1)) + title: qsTr("Settings") ControlCenter { id: cc diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml index fb71608..b65746b 100644 --- a/modules/controlcenter/WindowTitle.qml +++ b/modules/controlcenter/WindowTitle.qml @@ -19,7 +19,7 @@ StyledRect { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - text: qsTr("Caelestia Settings - %1").arg(root.session.active) + text: qsTr("Settings") font.capitalization: Font.Capitalize font.pointSize: Appearance.font.size.larger font.weight: 500 diff --git a/services/Network.qml b/services/Network.qml index 2c31065..f2c403e 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -27,13 +27,20 @@ Singleton { } function connectToNetwork(ssid: string, password: string): void { - // TODO: Implement password - connectProc.exec(["nmcli", "conn", "up", ssid]); + // First try to connect to an existing connection + // If that fails, create a new connection + if (password && password.length > 0) { + connectProc.exec(["nmcli", "device", "wifi", "connect", ssid, "password", password]); + } else { + // Try to connect to existing connection first + connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); + } } function disconnectFromNetwork(): void { if (active) { - disconnectProc.exec(["nmcli", "connection", "down", active.ssid]); + // Find the device name first, then disconnect + disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); } } @@ -86,6 +93,10 @@ Singleton { Process { id: connectProc + onExited: { + // Refresh network list after connection attempt + getNetworks.running = true; + } stdout: SplitParser { onRead: getNetworks.running = true } @@ -97,6 +108,10 @@ Singleton { Process { id: disconnectProc + onExited: { + // Refresh network list after disconnection + getNetworks.running = true; + } stdout: SplitParser { onRead: getNetworks.running = true } -- cgit v1.2.3-freya From 8783a3a95a7be02eb42d29441ff199b161d6cb82 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 9 Nov 2025 17:55:03 -0500 Subject: config: settings in bar --- config/BarConfig.qml | 4 ++++ modules/bar/Bar.qml | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/config/BarConfig.qml b/config/BarConfig.qml index 34819b1..745cda6 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -45,6 +45,10 @@ JsonObject { id: "statusIcons", enabled: true }, + { + id: "settings", + enabled: true + }, { id: "power", enabled: true diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 1dceea4..32e362b 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -158,6 +158,12 @@ ColumnLayout { sourceComponent: StatusIcons {} } } + DelegateChoice { + roleValue: "settings" + delegate: WrappedLoader { + sourceComponent: SettingsIcon {} + } + } DelegateChoice { roleValue: "power" delegate: WrappedLoader { -- cgit v1.2.3-freya From f1234bc96705b7f5bd35f2c1e5cfd532712f6f86 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 9 Nov 2025 19:10:11 -0500 Subject: fix: window title while keeping new panels modified: config/BarConfig.qml modified: modules/BatteryMonitor.qml modified: modules/background/DesktopClock.qml modified: modules/bar/Bar.qml modified: modules/controlcenter/WindowFactory.qml modified: modules/controlcenter/WindowTitle.qml --- config/BarConfig.qml | 4 ---- modules/BatteryMonitor.qml | 2 +- modules/background/DesktopClock.qml | 1 + modules/bar/Bar.qml | 6 ------ modules/controlcenter/WindowFactory.qml | 2 +- modules/controlcenter/WindowTitle.qml | 2 +- 6 files changed, 4 insertions(+), 13 deletions(-) diff --git a/config/BarConfig.qml b/config/BarConfig.qml index 745cda6..34819b1 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -45,10 +45,6 @@ JsonObject { id: "statusIcons", enabled: true }, - { - id: "settings", - enabled: true - }, { id: "power", enabled: true diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml index 7a3be12..d24cff2 100644 --- a/modules/BatteryMonitor.qml +++ b/modules/BatteryMonitor.qml @@ -15,7 +15,7 @@ Scope { function onOnBatteryChanged(): void { if (UPower.onBattery) { if (Config.utilities.toasts.chargingChanged) - Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is now on AC"), "power_off"); + Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off"); } else { if (Config.utilities.toasts.chargingChanged) Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power"); diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index 6dc6b6b..2de325c 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -12,6 +12,7 @@ Item { anchors.centerIn: parent text: Time.format(Config.services.useTwelveHourClock ? "hh:mm:ss A" : "hh:mm:ss") + font.family: Appearance.font.family.clock font.pointSize: Appearance.font.size.extraLarge font.bold: true } diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 32e362b..1dceea4 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -158,12 +158,6 @@ ColumnLayout { sourceComponent: StatusIcons {} } } - DelegateChoice { - roleValue: "settings" - delegate: WrappedLoader { - sourceComponent: SettingsIcon {} - } - } DelegateChoice { roleValue: "power" delegate: WrappedLoader { diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml index 14d85da..c5b7535 100644 --- a/modules/controlcenter/WindowFactory.qml +++ b/modules/controlcenter/WindowFactory.qml @@ -38,7 +38,7 @@ Singleton { implicitWidth: cc.implicitWidth implicitHeight: cc.implicitHeight - title: qsTr("Settings") + title: qsTr("Caelestia Settings - %1").arg(cc.active.slice(0, 1).toUpperCase() + cc.active.slice(1)) ControlCenter { id: cc diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml index b65746b..fb71608 100644 --- a/modules/controlcenter/WindowTitle.qml +++ b/modules/controlcenter/WindowTitle.qml @@ -19,7 +19,7 @@ StyledRect { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - text: qsTr("Settings") + text: qsTr("Caelestia Settings - %1").arg(root.session.active) font.capitalization: Font.Capitalize font.pointSize: Appearance.font.size.larger font.weight: 500 -- cgit v1.2.3-freya From 81a8157b67c4d2416d9c4550afc5b50c943352e4 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 9 Nov 2025 19:17:26 -0500 Subject: fix: panels (i'm debugging) new file: modules/bar/components/Settings.qml new file: modules/bar/components/SettingsIcon.qml new file: modules/controlcenter/audio/AudioPane.qml new file: modules/controlcenter/network/Details.qml new file: modules/controlcenter/network/NetworkList.qml new file: modules/controlcenter/network/NetworkPane.qml new file: modules/controlcenter/network/Settings.qml --- modules/bar/components/Settings.qml | 42 ++++ modules/bar/components/SettingsIcon.qml | 42 ++++ modules/controlcenter/audio/AudioPane.qml | 350 ++++++++++++++++++++++++++ modules/controlcenter/network/Details.qml | 204 +++++++++++++++ modules/controlcenter/network/NetworkList.qml | 240 ++++++++++++++++++ modules/controlcenter/network/NetworkPane.qml | 79 ++++++ modules/controlcenter/network/Settings.qml | 166 ++++++++++++ 7 files changed, 1123 insertions(+) create mode 100644 modules/bar/components/Settings.qml create mode 100644 modules/bar/components/SettingsIcon.qml create mode 100644 modules/controlcenter/audio/AudioPane.qml create mode 100644 modules/controlcenter/network/Details.qml create mode 100644 modules/controlcenter/network/NetworkList.qml create mode 100644 modules/controlcenter/network/NetworkPane.qml create mode 100644 modules/controlcenter/network/Settings.qml diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml new file mode 100644 index 0000000..e92fc46 --- /dev/null +++ b/modules/bar/components/Settings.qml @@ -0,0 +1,42 @@ +import qs.components +import qs.modules.controlcenter +import qs.services +import qs.config +import Quickshell +import QtQuick + +Item { + id: root + + implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: icon.implicitHeight + + StateLayer { + // Cursed workaround to make the height larger than the parent + anchors.fill: undefined + anchors.centerIn: parent + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + + radius: Appearance.rounding.full + + function onClicked(): void { + WindowFactory.create(); + } + } + + MaterialIcon { + id: icon + + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + + text: "settings" + color: Colours.palette.m3onSurface + font.bold: true + font.pointSize: Appearance.font.size.normal + } +} + + + diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml new file mode 100644 index 0000000..e92fc46 --- /dev/null +++ b/modules/bar/components/SettingsIcon.qml @@ -0,0 +1,42 @@ +import qs.components +import qs.modules.controlcenter +import qs.services +import qs.config +import Quickshell +import QtQuick + +Item { + id: root + + implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: icon.implicitHeight + + StateLayer { + // Cursed workaround to make the height larger than the parent + anchors.fill: undefined + anchors.centerIn: parent + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + + radius: Appearance.rounding.full + + function onClicked(): void { + WindowFactory.create(); + } + } + + MaterialIcon { + id: icon + + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + + text: "settings" + color: Colours.palette.m3onSurface + font.bold: true + font.pointSize: Appearance.font.size.normal + } +} + + + diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml new file mode 100644 index 0000000..6c478e5 --- /dev/null +++ b/modules/controlcenter/audio/AudioPane.qml @@ -0,0 +1,350 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property Session session + + anchors.fill: parent + + spacing: 0 + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true + + ColumnLayout { + 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 + + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + } + + StyledText { + text: qsTr("Output devices (%1)").arg(Audio.sinks.length) + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + text: qsTr("All available output devices") + color: Colours.palette.m3outline + } + + StyledListView { + id: outputView + + Layout.fillWidth: true + Layout.fillHeight: true + + model: Audio.sinks + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: outputView + } + + delegate: StyledRect { + required property var modelData + + anchors.left: parent.left + anchors.right: parent.right + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Audio.sink?.id === modelData.id ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: Audio.sink?.id === modelData.id ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + Audio.setAudioSink(modelData); + } + } + + RowLayout { + id: outputRowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + MaterialIcon { + text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group" + font.pointSize: Appearance.font.size.large + fill: Audio.sink?.id === modelData.id ? 1 : 0 + } + + StyledText { + Layout.fillWidth: true + + text: modelData.description || qsTr("Unknown") + font.weight: Audio.sink?.id === modelData.id ? 500 : 400 + } + } + + implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Input devices (%1)").arg(Audio.sources.length) + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + text: qsTr("All available input devices") + color: Colours.palette.m3outline + } + + StyledListView { + id: inputView + + Layout.fillWidth: true + Layout.fillHeight: true + + model: Audio.sources + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: inputView + } + + delegate: StyledRect { + required property var modelData + + anchors.left: parent.left + anchors.right: parent.right + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Audio.source?.id === modelData.id ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: Audio.source?.id === modelData.id ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + Audio.setAudioSource(modelData); + } + } + + RowLayout { + id: inputRowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + MaterialIcon { + text: Audio.source?.id === modelData.id ? "mic" : "mic_external_on" + font.pointSize: Appearance.font.size.large + fill: Audio.source?.id === modelData.id ? 1 : 0 + } + + StyledText { + Layout.fillWidth: true + + text: modelData.description || qsTr("Unknown") + font.weight: Audio.source?.id === modelData.id ? 500 : 400 + } + } + + implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + } + + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "volume_up" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Audio settings") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + Layout.alignment: Qt.AlignHCenter + text: qsTr("Output volume") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Volume") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + 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 + } + } + } + + StyledText { + text: Audio.muted ? qsTr("Muted") : qsTr("%1%").arg(Math.round(Audio.volume * 100)) + color: Audio.muted ? Colours.palette.m3primary : Colours.palette.m3outline + } + + StyledSlider { + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + value: Audio.volume + enabled: !Audio.muted + opacity: enabled ? 1 : 0.5 + onMoved: Audio.setVolume(value) + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + Layout.alignment: Qt.AlignHCenter + text: qsTr("Input volume") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Volume") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + 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: Audio.sourceMuted ? "mic_off" : "mic" + color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + } + } + } + + StyledText { + text: Audio.sourceMuted ? qsTr("Muted") : qsTr("%1%").arg(Math.round(Audio.sourceVolume * 100)) + color: Audio.sourceMuted ? Colours.palette.m3primary : Colours.palette.m3outline + } + + StyledSlider { + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + value: Audio.sourceVolume + enabled: !Audio.sourceMuted + opacity: enabled ? 1 : 0.5 + onMoved: Audio.setSourceVolume(value) + } + } + + InnerBorder { + leftThickness: Appearance.padding.normal / 2 + } + } +} + + diff --git a/modules/controlcenter/network/Details.qml b/modules/controlcenter/network/Details.qml new file mode 100644 index 0000000..86164f1 --- /dev/null +++ b/modules/controlcenter/network/Details.qml @@ -0,0 +1,204 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Session session + readonly property var network: session.network.active + + StyledFlickable { + anchors.fill: parent + + flickableDirection: Flickable.VerticalFlick + contentHeight: layout.height + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + animate: true + text: root.network?.isSecure ? "lock" : "wifi" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + animate: true + text: root.network?.ssid ?? qsTr("Unknown") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Connection status") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Connection settings for this network") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: networkStatus.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: networkStatus + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.larger + + Toggle { + label: qsTr("Connected") + checked: root.network?.active ?? false + toggle.onToggled: { + if (checked) { + if (root.network.isSecure) { + // TODO: Show password dialog + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = root.network; + } else { + Network.connectToNetwork(root.network.ssid, ""); + } + } else { + Network.disconnectFromNetwork(); + } + } + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Network properties") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Additional information") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: networkProps.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: networkProps + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("SSID") + } + + StyledText { + text: root.network?.ssid ?? qsTr("Unknown") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("BSSID") + } + + StyledText { + text: root.network?.bssid ?? qsTr("Unknown") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Signal strength") + } + + StyledText { + text: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Frequency") + } + + StyledText { + text: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Security") + } + + StyledText { + text: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + } + } + } + } + + component Toggle: RowLayout { + required property string label + property alias checked: toggle.checked + property alias toggle: toggle + + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: parent.label + } + + StyledSwitch { + id: toggle + + cLayer: 2 + } + } +} + diff --git a/modules/controlcenter/network/NetworkList.qml b/modules/controlcenter/network/NetworkList.qml new file mode 100644 index 0000000..8dfebc7 --- /dev/null +++ b/modules/controlcenter/network/NetworkList.qml @@ -0,0 +1,240 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: wifiIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: Network.wifiEnabled ? Colours.palette.m3tertiary : Colours.palette.m3tertiaryContainer + + StateLayer { + function onClicked(): void { + Network.toggleWifi(); + } + } + + MaterialIcon { + id: wifiIcon + + anchors.centerIn: parent + text: "wifi" + color: Network.wifiEnabled ? Colours.palette.m3onTertiary : Colours.palette.m3onTertiaryContainer + } + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: scanIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: Network.scanning ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + + StateLayer { + function onClicked(): void { + Network.rescanWifi(); + } + } + + MaterialIcon { + id: scanIcon + + anchors.centerIn: parent + text: "wifi_find" + color: Network.scanning ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + } + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: settingsIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: !root.session.network.active ? Colours.palette.m3primary : Colours.palette.m3primaryContainer + + StateLayer { + function onClicked(): void { + if (root.session.network.active) + root.session.network.active = null; + else { + root.session.network.active = view.model.get(0)?.modelData ?? null; + } + } + } + + MaterialIcon { + id: settingsIcon + + anchors.centerIn: parent + text: "settings" + color: !root.session.network.active ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Networks (%1)").arg(Network.networks.length) + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + visible: Network.scanning + text: qsTr("Scanning...") + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.small + } + } + + StyledText { + text: qsTr("All available WiFi networks") + color: Colours.palette.m3outline + } + + StyledListView { + id: view + + Layout.fillWidth: true + Layout.fillHeight: true + + model: Network.networks + + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: view + } + + delegate: StyledRect { + required property var modelData + + anchors.left: parent.left + anchors.right: parent.right + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.network.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: root.session.network.active === modelData ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + root.session.network.active = modelData; + } + } + + RowLayout { + id: rowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: modelData.isSecure ? "lock" : "wifi" + font.pointSize: Appearance.font.size.large + fill: modelData.active ? 1 : 0 + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + + StyledText { + Layout.fillWidth: true + + text: modelData.ssid || qsTr("Unknown") + } + + StyledText { + text: modelData.active ? qsTr("Connected") : (modelData.isSecure ? qsTr("Secured") : qsTr("Open")) + color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.active ? 500 : 400 + } + + StyledText { + text: qsTr("%1%").arg(modelData.strength) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) + + StateLayer { + function onClicked(): void { + if (modelData.active) { + Network.disconnectFromNetwork(); + } else { + if (modelData.isSecure) { + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = modelData; + } else { + Network.connectToNetwork(modelData.ssid, ""); + } + } + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + text: modelData.active ? "link_off" : "link" + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + } + + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } +} diff --git a/modules/controlcenter/network/NetworkPane.qml b/modules/controlcenter/network/NetworkPane.qml new file mode 100644 index 0000000..f37eedd --- /dev/null +++ b/modules/controlcenter/network/NetworkPane.qml @@ -0,0 +1,79 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.effects +import qs.components.containers +import qs.config +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property Session session + + anchors.fill: parent + + spacing: 0 + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true + + NetworkList { + 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 + + session: root.session + } + + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + Loader { + id: loader + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + sourceComponent: root.session.network.active ? details : settings + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + } + + InnerBorder { + leftThickness: Appearance.padding.normal / 2 + } + + Component { + id: settings + + Settings { + session: root.session + } + } + + Component { + id: details + + Details { + session: root.session + } + } + } +} diff --git a/modules/controlcenter/network/Settings.qml b/modules/controlcenter/network/Settings.qml new file mode 100644 index 0000000..0794e6a --- /dev/null +++ b/modules/controlcenter/network/Settings.qml @@ -0,0 +1,166 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "wifi" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Network settings") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("WiFi status") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("General WiFi settings") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: wifiStatus.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: wifiStatus + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.larger + + Toggle { + label: qsTr("WiFi enabled") + checked: Network.wifiEnabled + toggle.onToggled: { + Network.enableWifi(checked); + } + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Network information") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Current network connection") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: networkInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: networkInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("Connected network") + } + + StyledText { + text: Network.active ? Network.active.ssid : qsTr("Not connected") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Signal strength") + } + + StyledText { + text: Network.active ? qsTr("%1%").arg(Network.active.strength) : qsTr("N/A") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Security") + } + + StyledText { + text: Network.active ? (Network.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Frequency") + } + + StyledText { + text: Network.active ? qsTr("%1 MHz").arg(Network.active.frequency) : qsTr("N/A") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + } + } + + component Toggle: RowLayout { + required property string label + property alias checked: toggle.checked + property alias toggle: toggle + + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: parent.label + } + + StyledSwitch { + id: toggle + + cLayer: 2 + } + } +} + -- cgit v1.2.3-freya From d8de7c7e4b1b57470297dc238b210670f87c23ba Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 9 Nov 2025 21:43:51 -0500 Subject: controlcenter: wifi passphrase input fix: dependability issues with nmcli --- modules/controlcenter/network/Details.qml | 30 ++- modules/controlcenter/network/NetworkList.qml | 29 ++- modules/controlcenter/network/NetworkPane.qml | 6 + modules/controlcenter/network/PasswordDialog.qml | 241 +++++++++++++++++++++++ services/Network.qml | 158 ++++++++++++++- 5 files changed, 451 insertions(+), 13 deletions(-) create mode 100644 modules/controlcenter/network/PasswordDialog.qml diff --git a/modules/controlcenter/network/Details.qml b/modules/controlcenter/network/Details.qml index 86164f1..19e011f 100644 --- a/modules/controlcenter/network/Details.qml +++ b/modules/controlcenter/network/Details.qml @@ -79,17 +79,37 @@ Item { checked: root.network?.active ?? false toggle.onToggled: { if (checked) { - if (root.network.isSecure) { - // TODO: Show password dialog - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = root.network; + // If already connected to a different network, disconnect first + if (Network.active && Network.active.ssid !== root.network.ssid) { + Network.disconnectFromNetwork(); + // Wait a moment before connecting to new network + Qt.callLater(() => { + connectToNetwork(); + }); } else { - Network.connectToNetwork(root.network.ssid, ""); + connectToNetwork(); } } else { Network.disconnectFromNetwork(); } } + + function connectToNetwork(): void { + if (root.network.isSecure) { + // Try connecting without password first (in case it's saved) + Network.connectToNetworkWithPasswordCheck( + root.network.ssid, + root.network.isSecure, + () => { + // Callback: connection failed, show password dialog + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = root.network; + } + ); + } else { + Network.connectToNetwork(root.network.ssid, ""); + } + } } } } diff --git a/modules/controlcenter/network/NetworkList.qml b/modules/controlcenter/network/NetworkList.qml index 8dfebc7..df05de7 100644 --- a/modules/controlcenter/network/NetworkList.qml +++ b/modules/controlcenter/network/NetworkList.qml @@ -214,14 +214,35 @@ ColumnLayout { if (modelData.active) { Network.disconnectFromNetwork(); } else { - if (modelData.isSecure) { - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = modelData; + // If already connected to a different network, disconnect first + if (Network.active && Network.active.ssid !== modelData.ssid) { + Network.disconnectFromNetwork(); + // Wait a moment before connecting to new network + Qt.callLater(() => { + connectToNetwork(); + }); } else { - Network.connectToNetwork(modelData.ssid, ""); + connectToNetwork(); } } } + + function connectToNetwork(): void { + if (modelData.isSecure) { + // Try connecting without password first (in case it's saved) + Network.connectToNetworkWithPasswordCheck( + modelData.ssid, + modelData.isSecure, + () => { + // Callback: connection failed, show password dialog + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = modelData; + } + ); + } else { + Network.connectToNetwork(modelData.ssid, ""); + } + } } MaterialIcon { diff --git a/modules/controlcenter/network/NetworkPane.qml b/modules/controlcenter/network/NetworkPane.qml index f37eedd..5e8a75a 100644 --- a/modules/controlcenter/network/NetworkPane.qml +++ b/modules/controlcenter/network/NetworkPane.qml @@ -76,4 +76,10 @@ RowLayout { } } } + + PasswordDialog { + anchors.fill: parent + session: root.session + z: 1000 + } } diff --git a/modules/controlcenter/network/PasswordDialog.qml b/modules/controlcenter/network/PasswordDialog.qml new file mode 100644 index 0000000..fa4788c --- /dev/null +++ b/modules/controlcenter/network/PasswordDialog.qml @@ -0,0 +1,241 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Session session + readonly property var network: session.network.pendingNetwork + + visible: session.network.showPasswordDialog + enabled: visible + focus: visible + + Keys.onEscapePressed: { + root.session.network.showPasswordDialog = false; + passwordField.text = ""; + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.5) + opacity: root.visible ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + root.session.network.showPasswordDialog = false; + passwordField.text = ""; + } + } + } + + StyledRect { + id: dialog + + anchors.centerIn: parent + + implicitWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surface + opacity: root.visible ? 1 : 0 + scale: root.visible ? 1 : 0.9 + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + + Behavior on scale { + NumberAnimation { + duration: 200 + } + } + + Keys.onEscapePressed: { + root.session.network.showPasswordDialog = false; + passwordField.text = ""; + } + + ColumnLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "lock" + font.pointSize: Appearance.font.size.extraLarge * 2 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enter password") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + Item { + Layout.topMargin: Appearance.spacing.large + Layout.fillWidth: true + implicitHeight: passwordField.implicitHeight + Appearance.padding.normal * 2 + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + border.width: passwordField.activeFocus ? 2 : 1 + border.color: passwordField.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline + + Behavior on border.color { + CAnim {} + } + } + + StyledTextField { + id: passwordField + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + echoMode: TextField.Password + placeholderText: qsTr("Password") + + Component.onCompleted: { + if (root.visible) { + forceActiveFocus(); + } + } + + Connections { + target: root + function onVisibleChanged(): void { + if (root.visible) { + passwordField.forceActiveFocus(); + passwordField.text = ""; + } + } + } + + Keys.onReturnPressed: { + if (connectButton.enabled) { + connectButton.clicked(); + } + } + Keys.onEnterPressed: { + if (connectButton.enabled) { + connectButton.clicked(); + } + } + } + } + + RowLayout { + Layout.topMargin: Appearance.spacing.normal + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + Button { + id: cancelButton + + Layout.fillWidth: true + color: Colours.palette.m3secondaryContainer + onColor: Colours.palette.m3onSecondaryContainer + text: qsTr("Cancel") + + function onClicked(): void { + root.session.network.showPasswordDialog = false; + passwordField.text = ""; + } + } + + Button { + id: connectButton + + Layout.fillWidth: true + color: Colours.palette.m3primary + onColor: Colours.palette.m3onPrimary + text: qsTr("Connect") + enabled: passwordField.text.length > 0 + + function onClicked(): void { + if (root.network && passwordField.text.length > 0) { + Network.connectToNetwork(root.network.ssid, passwordField.text); + root.session.network.showPasswordDialog = false; + passwordField.text = ""; + } + } + } + } + } + } + + component Button: StyledRect { + property color onColor: Colours.palette.m3onSurface + property alias disabled: stateLayer.disabled + property alias text: label.text + property alias enabled: stateLayer.enabled + + function onClicked(): void { + } + + radius: Appearance.rounding.small + implicitHeight: label.implicitHeight + Appearance.padding.small * 2 + opacity: enabled ? 1 : 0.5 + + StateLayer { + id: stateLayer + + enabled: parent.enabled + color: parent.onColor + + function onClicked(): void { + if (enabled) { + parent.onClicked(); + } + } + } + + StyledText { + id: label + + anchors.centerIn: parent + animate: true + color: parent.onColor + font.pointSize: Appearance.font.size.normal + } + } +} + diff --git a/services/Network.qml b/services/Network.qml index f2c403e..3ceadab 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -26,20 +26,42 @@ Singleton { rescanProc.running = true; } + property var pendingConnection: null + signal connectionFailed(string ssid) + function connectToNetwork(ssid: string, password: string): void { // First try to connect to an existing connection // If that fails, create a new connection if (password && password.length > 0) { connectProc.exec(["nmcli", "device", "wifi", "connect", ssid, "password", password]); } else { - // Try to connect to existing connection first + // Try to connect to existing connection first (will use saved password if available) + connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); + } + } + + function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var): void { + // For secure networks, try connecting without password first + // If connection succeeds (saved password exists), we're done + // If it fails with password error, callback will be called to show password dialog + if (isSecure) { + root.pendingConnection = { ssid: ssid, callback: callback }; + // Try connecting without password - will use saved password if available connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); + // Start timer to check if connection succeeded + connectionCheckTimer.start(); + } else { + connectToNetwork(ssid, ""); } } function disconnectFromNetwork(): void { - if (active) { - // Find the device name first, then disconnect + // Try to disconnect - use connection name if available, otherwise use device + if (active && active.ssid) { + // First try to disconnect by connection name (more reliable) + disconnectByConnectionProc.exec(["nmcli", "connection", "down", active.ssid]); + } else { + // Fallback: disconnect by device disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); } } @@ -90,18 +112,102 @@ Singleton { } } + Timer { + id: connectionCheckTimer + interval: 4000 + onTriggered: { + if (root.pendingConnection) { + // Final check - if connection still hasn't succeeded, show password dialog + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + if (!connected && root.pendingConnection.callback) { + // Connection didn't succeed after multiple checks, show password dialog + const pending = root.pendingConnection; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + pending.callback(); + } else if (connected) { + // Connection succeeded, clear pending + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + } + + Timer { + id: immediateCheckTimer + interval: 500 + repeat: true + triggeredOnStart: false + property int checkCount: 0 + onTriggered: { + if (root.pendingConnection) { + checkCount++; + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + if (connected) { + // Connection succeeded, stop timers and clear pending + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.pendingConnection = null; + } else if (checkCount >= 6) { + // Checked 6 times (3 seconds total), connection likely failed + // Stop immediate check, let the main timer handle it + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } else { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + Process { id: connectProc onExited: { // Refresh network list after connection attempt getNetworks.running = true; + + // Check if connection succeeded after a short delay (network list needs to update) + if (root.pendingConnection) { + immediateCheckTimer.start(); + } } stdout: SplitParser { onRead: getNetworks.running = true } stderr: StdioCollector { - onStreamFinished: console.warn("Network connection error:", text) + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + // Check for specific errors that indicate password is needed + // Be careful not to match success messages + const needsPassword = (error.includes("Secrets were required") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + (error.includes("password") && !error.includes("Connection activated")) || + (error.includes("Secrets") && !error.includes("Connection activated")) || + (error.includes("802.11") && !error.includes("Connection activated"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { + // Connection failed because password is needed - show dialog immediately + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + const pending = root.pendingConnection; + root.pendingConnection = null; + pending.callback(); + } else if (error && error.length > 0 && !error.includes("Connection activated")) { + // Only log non-success messages + console.warn("Network connection error:", error); + } + } + } } } @@ -115,6 +221,36 @@ Singleton { stdout: SplitParser { onRead: getNetworks.running = true } + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { + console.warn("Network device disconnect error:", error); + } + } + } + } + + Process { + id: disconnectByConnectionProc + + onExited: { + // Refresh network list after disconnection + getNetworks.running = true; + } + stdout: SplitParser { + onRead: getNetworks.running = true + } + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { + console.warn("Network connection disconnect error:", error); + // If connection down failed, try device disconnect as fallback + disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); + } + } + } } Process { @@ -182,6 +318,20 @@ Singleton { })); } } + + // Check if pending connection succeeded after network list is fully updated + if (root.pendingConnection) { + Qt.callLater(() => { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + if (connected) { + // Connection succeeded, stop timers and clear pending + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.pendingConnection = null; + } + }); + } } } } -- cgit v1.2.3-freya From 8d7bb827ebd8284706577518e2e58d6a61972889 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 9 Nov 2025 22:48:38 -0500 Subject: controlcenter: appearance pane --- modules/controlcenter/NavRail.qml | 5 + modules/controlcenter/Panes.qml | 8 + modules/controlcenter/Session.qml | 2 +- .../controlcenter/appearance/AppearancePane.qml | 520 +++++++++++++++++++++ 4 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 modules/controlcenter/appearance/AppearancePane.qml diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 70438a6..96bbb65 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -171,6 +171,11 @@ Item { icon: "volume_up" label: "audio" } + + NavItem { + icon: "palette" + label: "appearance" + } } component NavItem: Item { diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index cc4a4cb..5b1039c 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import "bluetooth" import "network" import "audio" +import "appearance" import qs.components import qs.services import qs.config @@ -44,6 +45,13 @@ ClippingRectangle { } } + Pane { + index: 3 + sourceComponent: AppearancePane { + session: root.session + } + } + Behavior on y { Anim {} } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index b716b20..4ac09a4 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -2,7 +2,7 @@ import Quickshell.Bluetooth import QtQuick QtObject { - readonly property list panes: ["network", "bluetooth", "audio"] + readonly property list panes: ["network", "bluetooth", "audio", "appearance"] required property var root property bool floating: false diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml new file mode 100644 index 0000000..ab00345 --- /dev/null +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -0,0 +1,520 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../launcher/services" +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.components.images +import qs.services +import qs.config +import Caelestia.Models +import Quickshell +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property Session session + + anchors.fill: parent + + spacing: 0 + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true + + ColumnLayout { + 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 + + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Theme mode") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Light or dark theme") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: modeToggle.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + RowLayout { + id: modeToggle + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Dark mode") + } + + StyledSwitch { + checked: !Colours.currentLight + onToggled: { + Colours.setMode(checked ? "dark" : "light"); + } + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Color variant") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Material theme variant") + color: Colours.palette.m3outline + } + + StyledListView { + Layout.fillWidth: true + Layout.fillHeight: true + + model: M3Variants.list + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + delegate: StyledRect { + required property var modelData + + anchors.left: parent.left + anchors.right: parent.right + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + Quickshell.execDetached(["caelestia", "scheme", "set", "-v", modelData.variant]); + Schemes.reload(); + } + } + + RowLayout { + id: variantRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + MaterialIcon { + text: modelData.icon + font.pointSize: Appearance.font.size.large + fill: modelData.variant === Schemes.currentVariant ? 1 : 0 + } + + StyledText { + Layout.fillWidth: true + text: modelData.name + font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 + } + + MaterialIcon { + visible: modelData.variant === Schemes.currentVariant + text: "check" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.large + } + } + + implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Color scheme") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Available color schemes") + color: Colours.palette.m3outline + } + + StyledListView { + Layout.fillWidth: true + Layout.fillHeight: true + + model: Schemes.list + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + delegate: StyledRect { + required property var modelData + + anchors.left: parent.left + anchors.right: parent.right + + readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` + readonly property bool isCurrent: schemeKey === Schemes.currentScheme + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + Quickshell.execDetached(["caelestia", "scheme", "set", "-n", modelData.name, "-f", modelData.flavour]); + Schemes.reload(); + } + } + + RowLayout { + id: schemeRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + Item { + readonly property real itemHeight: schemeRow.implicitHeight || 50 + Layout.preferredWidth: itemHeight * 0.8 + Layout.preferredHeight: itemHeight * 0.8 + + StyledRect { + id: preview + + anchors.verticalCenter: parent.verticalCenter + + border.width: 1 + border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) + + color: `#${modelData.colours?.surface}` + radius: Appearance.rounding.full + implicitWidth: parent.itemHeight * 0.8 + implicitHeight: parent.itemHeight * 0.8 + + Item { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + implicitWidth: parent.implicitWidth / 2 + clip: true + + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + implicitWidth: preview.implicitWidth + color: `#${modelData.colours?.primary}` + radius: Appearance.rounding.full + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + text: modelData.name + font.weight: isCurrent ? 500 : 400 + } + + StyledText { + text: modelData.flavour + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline + } + } + + MaterialIcon { + visible: isCurrent + text: "check" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.large + } + } + + implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } + + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + StyledFlickable { + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.implicitHeight + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + ColumnLayout { + id: contentLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + 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("Theme mode") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Colours.currentLight ? qsTr("Light mode") : qsTr("Dark mode") + color: Colours.palette.m3outline + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + Layout.alignment: Qt.AlignHCenter + text: qsTr("Wallpaper") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Select a wallpaper") + color: Colours.palette.m3outline + } + + GridLayout { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + Layout.alignment: Qt.AlignHCenter + + columns: Math.max(1, Math.floor(parent.width / 200)) + rowSpacing: Appearance.spacing.normal + columnSpacing: Appearance.spacing.normal + + // Center the grid content + Layout.maximumWidth: { + const cols = columns; + const itemWidth = 180; + const spacing = columnSpacing; + return cols * itemWidth + (cols - 1) * spacing; + } + + Repeater { + model: Wallpapers.list + + delegate: Item { + required property var modelData + + Layout.preferredWidth: 180 + Layout.preferredHeight: 120 + Layout.minimumWidth: 180 + Layout.minimumHeight: 120 + + readonly property bool isCurrent: modelData.path === Wallpapers.actualCurrent + readonly property real imageWidth: Math.max(1, width) + readonly property real imageHeight: Math.max(1, height) + + StateLayer { + radius: Appearance.rounding.normal + + function onClicked(): void { + Wallpapers.setWallpaper(modelData.path); + } + } + + StyledClippingRect { + id: image + + anchors.fill: parent + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal + + border.width: isCurrent ? 2 : 0 + border.color: Colours.palette.m3primary + + CachingImage { + id: cachingImage + + path: modelData.path + anchors.fill: parent + + // Ensure sourceSize is always set to valid dimensions + sourceSize: Qt.size( + Math.max(1, Math.floor(parent.width)), + Math.max(1, Math.floor(parent.height)) + ) + + // Show when ready, hide if fallback is showing + opacity: status === Image.Ready && !fallbackImage.visible ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + } + + // Fallback: Direct image load if caching fails or is slow + Image { + id: fallbackImage + + anchors.fill: parent + source: modelData.path + asynchronous: true + fillMode: Image.PreserveAspectCrop + sourceSize: Qt.size( + Math.max(1, Math.floor(parent.width)), + Math.max(1, Math.floor(parent.height)) + ) + + // Show if caching image hasn't loaded after a delay + visible: opacity > 0 + opacity: 0 + + Timer { + id: fallbackTimer + interval: 500 + running: cachingImage.status === Image.Loading || (cachingImage.status !== Image.Ready && cachingImage.status !== Image.Null) + onTriggered: { + if (cachingImage.status !== Image.Ready && fallbackImage.status === Image.Ready) { + fallbackImage.opacity = 1; + } + } + } + + // Also check status changes + onStatusChanged: { + if (status === Image.Ready && cachingImage.status !== Image.Ready) { + Qt.callLater(() => { + if (cachingImage.status !== Image.Ready) { + fallbackImage.opacity = 1; + } + }); + } + } + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + } + + 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 { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Appearance.padding.small + + text: modelData.relativePath + font.pointSize: Appearance.font.size.small + color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface + elide: Text.ElideRight + } + } + } + } + } + } + + InnerBorder { + leftThickness: Appearance.padding.normal / 2 + } + } +} + + -- cgit v1.2.3-freya From d20a5095bb38d843c7f6327a553b9929e229a3b5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 9 Nov 2025 22:52:21 -0500 Subject: fix: color schemes and variants in appearance pane --- .../controlcenter/appearance/AppearancePane.qml | 42 ++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index ab00345..b2692c8 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -130,7 +130,25 @@ RowLayout { StateLayer { function onClicked(): void { - Quickshell.execDetached(["caelestia", "scheme", "set", "-v", modelData.variant]); + const variant = modelData.variant; + + // Optimistic update - set immediately + Schemes.currentVariant = variant; + + // Execute the command + Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); + + // Reload after a delay to confirm + Qt.callLater(() => { + reloadTimer.restart(); + }); + } + } + + Timer { + id: reloadTimer + interval: 300 + onTriggered: { Schemes.reload(); } } @@ -209,7 +227,27 @@ RowLayout { StateLayer { function onClicked(): void { - Quickshell.execDetached(["caelestia", "scheme", "set", "-n", modelData.name, "-f", modelData.flavour]); + const name = modelData.name; + const flavour = modelData.flavour; + const schemeKey = `${name} ${flavour}`; + + // Optimistic update - set immediately + Schemes.currentScheme = schemeKey; + + // Execute the command + Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); + + // Reload after a delay to confirm + Qt.callLater(() => { + reloadTimer.restart(); + }); + } + } + + Timer { + id: reloadTimer + interval: 300 + onTriggered: { Schemes.reload(); } } -- cgit v1.2.3-freya From 615d1d9a495604d229764c225b363dbaaaacb2bc Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 9 Nov 2025 23:36:44 -0500 Subject: controlcenter: cleaning up ui to match --- modules/controlcenter/network/NetworkList.qml | 153 ++++++++++++++++---------- modules/controlcenter/network/NetworkPane.qml | 91 +++++++++++++-- 2 files changed, 176 insertions(+), 68 deletions(-) diff --git a/modules/controlcenter/network/NetworkList.qml b/modules/controlcenter/network/NetworkList.qml index df05de7..f528092 100644 --- a/modules/controlcenter/network/NetworkList.qml +++ b/modules/controlcenter/network/NetworkList.qml @@ -29,74 +29,38 @@ ColumnLayout { Layout.fillWidth: true } - StyledRect { - implicitWidth: implicitHeight - implicitHeight: wifiIcon.implicitHeight + Appearance.padding.normal * 2 + ToggleButton { + toggled: Network.wifiEnabled + icon: "wifi" + accent: "Tertiary" - radius: Appearance.rounding.normal - color: Network.wifiEnabled ? Colours.palette.m3tertiary : Colours.palette.m3tertiaryContainer - - StateLayer { - function onClicked(): void { - Network.toggleWifi(); - } - } - - MaterialIcon { - id: wifiIcon - - anchors.centerIn: parent - text: "wifi" - color: Network.wifiEnabled ? Colours.palette.m3onTertiary : Colours.palette.m3onTertiaryContainer + function onClicked(): void { + Network.toggleWifi(); } } - StyledRect { - implicitWidth: implicitHeight - implicitHeight: scanIcon.implicitHeight + Appearance.padding.normal * 2 - - radius: Appearance.rounding.normal - color: Network.scanning ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + ToggleButton { + toggled: Network.scanning + icon: "wifi_find" + accent: "Secondary" - StateLayer { - function onClicked(): void { - Network.rescanWifi(); - } - } - - MaterialIcon { - id: scanIcon - - anchors.centerIn: parent - text: "wifi_find" - color: Network.scanning ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + function onClicked(): void { + Network.rescanWifi(); } } - StyledRect { - implicitWidth: implicitHeight - implicitHeight: settingsIcon.implicitHeight + Appearance.padding.normal * 2 + ToggleButton { + toggled: !root.session.network.active + icon: "settings" + accent: "Primary" - radius: Appearance.rounding.normal - color: !root.session.network.active ? Colours.palette.m3primary : Colours.palette.m3primaryContainer - - StateLayer { - function onClicked(): void { - if (root.session.network.active) - root.session.network.active = null; - else { - root.session.network.active = view.model.get(0)?.modelData ?? null; - } + function onClicked(): void { + if (root.session.network.active) + root.session.network.active = null; + else { + root.session.network.active = view.model.get(0)?.modelData ?? null; } } - - MaterialIcon { - id: settingsIcon - - anchors.centerIn: parent - text: "settings" - color: !root.session.network.active ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer - } } } @@ -258,4 +222,79 @@ ColumnLayout { implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } + + component ToggleButton: StyledRect { + id: toggleBtn + + required property bool toggled + property string icon + property string label + property string accent: "Secondary" + + function onClicked(): void { + } + + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) + implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2 + implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) + color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] + + StateLayer { + id: toggleStateLayer + + color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] + + function onClicked(): void { + toggleBtn.onClicked(); + } + } + + RowLayout { + id: toggleBtnInner + + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + id: toggleBtnIcon + + visible: !!text + fill: toggleBtn.toggled ? 1 : 0 + text: toggleBtn.icon + color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] + font.pointSize: Appearance.font.size.large + + Behavior on fill { + Anim {} + } + } + + Loader { + asynchronous: true + active: !!toggleBtn.label + visible: active + + sourceComponent: StyledText { + text: toggleBtn.label + color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] + } + } + } + + Behavior on radius { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + Behavior on Layout.preferredWidth { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } } diff --git a/modules/controlcenter/network/NetworkPane.qml b/modules/controlcenter/network/NetworkPane.qml index 5e8a75a..fe18274 100644 --- a/modules/controlcenter/network/NetworkPane.qml +++ b/modules/controlcenter/network/NetworkPane.qml @@ -5,6 +5,7 @@ import qs.components import qs.components.effects import qs.components.containers import qs.config +import Quickshell.Widgets import QtQuick import QtQuick.Layouts @@ -41,30 +42,92 @@ RowLayout { Layout.fillWidth: true Layout.fillHeight: true - Loader { - id: loader - + ClippingRectangle { anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - - sourceComponent: root.session.network.active ? details : settings + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + + radius: rightBorder.innerRadius + color: "transparent" + + Loader { + id: loader + + property var pane: root.session.network.active + property string paneId: pane ? (pane.ssid || pane.bssid || "") : "" + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + + clip: false + asynchronous: true + sourceComponent: pane ? details : settings + + 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 + } + } + PropertyAction {} + 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 + } + } + } + } - Behavior on opacity { - NumberAnimation { - duration: 200 + onPaneChanged: { + paneId = pane ? (pane.ssid || pane.bssid || "") : ""; } } } InnerBorder { + id: rightBorder + leftThickness: Appearance.padding.normal / 2 } Component { id: settings - Settings { - session: root.session + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + + Settings { + id: settingsInner + + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } } } @@ -82,4 +145,10 @@ RowLayout { session: root.session z: 1000 } + + component Anim: NumberAnimation { + target: loader + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + } } -- cgit v1.2.3-freya From 4897b3788227a6b12d6c18b50da200e9cba86c20 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 9 Nov 2025 23:51:53 -0500 Subject: osicon: toggle launcher on click --- modules/bar/components/OsIcon.qml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml index ed6da5d..2bc3864 100644 --- a/modules/bar/components/OsIcon.qml +++ b/modules/bar/components/OsIcon.qml @@ -2,9 +2,27 @@ import qs.components.effects import qs.services import qs.config import qs.utils +import QtQuick -ColouredIcon { - source: SysInfo.osLogo - implicitSize: Appearance.font.size.large * 1.2 - colour: Colours.palette.m3tertiary +Item { + id: root + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + const visibilities = Visibilities.getForActive(); + visibilities.launcher = !visibilities.launcher; + } + } + + ColouredIcon { + anchors.centerIn: parent + source: SysInfo.osLogo + implicitSize: Appearance.font.size.large * 1.2 + colour: Colours.palette.m3tertiary + } + + implicitWidth: Appearance.font.size.large * 1.2 + implicitHeight: Appearance.font.size.large * 1.2 } -- cgit v1.2.3-freya From 47dfcf4baf42c33ca5ee78dd4c0877e894aec1fb Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 10 Nov 2025 00:46:45 -0500 Subject: launcher: python execution like calculator functionality --- config/LauncherConfig.qml | 8 +++ modules/launcher/AppList.qml | 18 ++++- modules/launcher/Content.qml | 2 +- modules/launcher/items/PythonItem.qml | 124 ++++++++++++++++++++++++++++++++++ 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 modules/launcher/items/PythonItem.qml diff --git a/config/LauncherConfig.qml b/config/LauncherConfig.qml index 9d9c50c..30b3e8b 100644 --- a/config/LauncherConfig.qml +++ b/config/LauncherConfig.qml @@ -38,6 +38,14 @@ JsonObject { enabled: true, dangerous: false }, + { + name: "Python", + icon: "code", + description: "Execute Python one-liners", + command: ["autocomplete", "python"], + enabled: true, + dangerous: false + }, { name: "Scheme", icon: "palette", diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index 7f7b843..6217237 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -52,7 +52,7 @@ StyledListView { const text = search.text; const prefix = Config.launcher.actionPrefix; if (text.startsWith(prefix)) { - for (const action of ["calc", "scheme", "variant"]) + for (const action of ["calc", "python", "scheme", "variant"]) if (text.startsWith(`${prefix}${action} `)) return action; @@ -92,6 +92,14 @@ StyledListView { root.delegate: calcItem } }, + State { + name: "python" + + PropertyChanges { + model.values: [0] + root.delegate: pythonItem + } + }, State { name: "scheme" @@ -239,6 +247,14 @@ StyledListView { } } + Component { + id: pythonItem + + PythonItem { + list: root + } + } + Component { id: schemeItem diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index f674569..3a90a74 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -90,7 +90,7 @@ Item { Wallpapers.setWallpaper(currentItem.modelData.path); root.visibilities.launcher = false; } else if (text.startsWith(Config.launcher.actionPrefix)) { - if (text.startsWith(`${Config.launcher.actionPrefix}calc `)) + if (text.startsWith(`${Config.launcher.actionPrefix}calc `) || text.startsWith(`${Config.launcher.actionPrefix}python `)) currentItem.onClicked(); else currentItem.modelData.onClicked(list.currentList); diff --git a/modules/launcher/items/PythonItem.qml b/modules/launcher/items/PythonItem.qml new file mode 100644 index 0000000..8384e71 --- /dev/null +++ b/modules/launcher/items/PythonItem.qml @@ -0,0 +1,124 @@ +import qs.components +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property var list + readonly property string code: list.search.text.slice(`${Config.launcher.actionPrefix}python `.length) + + function onClicked(): void { + // Execute Python code and copy result to clipboard + // Escape single quotes in code for shell safety + const escapedCode = root.code.replace(/'/g, "'\\''"); + Quickshell.execDetached(["sh", "-c", `python3 -c '${escapedCode}' 2>&1 | wl-copy`]); + root.list.visibilities.launcher = false; + } + + implicitHeight: Config.launcher.sizes.itemHeight + + anchors.left: parent?.left + anchors.right: parent?.right + + StateLayer { + radius: Appearance.rounding.normal + + function onClicked(): void { + root.onClicked(); + } + } + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.larger + + spacing: Appearance.spacing.normal + + MaterialIcon { + text: "code" + font.pointSize: Appearance.font.size.extraLarge + Layout.alignment: Qt.AlignVCenter + } + + StyledText { + id: result + + color: { + if (!root.code) + return Colours.palette.m3onSurfaceVariant; + return Colours.palette.m3onSurface; + } + + text: root.code.length > 0 ? qsTr("Press Enter to execute: %1").arg(root.code) : qsTr("Type Python code to execute") + elide: Text.ElideLeft + + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + + StyledRect { + color: Colours.palette.m3tertiary + radius: Appearance.rounding.normal + clip: true + + implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Appearance.padding.small * 2 + + Layout.alignment: Qt.AlignVCenter + + StateLayer { + id: stateLayer + + color: Colours.palette.m3onTertiary + + function onClicked(): void { + const escapedCode = root.code.replace(/'/g, "'\\''"); + Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `python3 -i -c '${escapedCode}'`]); + root.list.visibilities.launcher = false; + } + } + + StyledText { + id: label + + anchors.verticalCenter: parent.verticalCenter + anchors.right: icon.left + anchors.rightMargin: Appearance.spacing.small + + text: qsTr("Open in terminal") + color: Colours.palette.m3onTertiary + font.pointSize: Appearance.font.size.normal + + opacity: stateLayer.containsMouse ? 1 : 0 + + Behavior on opacity { + Anim {} + } + } + + MaterialIcon { + id: icon + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: Appearance.padding.normal + + text: "open_in_new" + color: Colours.palette.m3onTertiary + font.pointSize: Appearance.font.size.large + } + + Behavior on implicitWidth { + Anim { + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + } + } +} -- cgit v1.2.3-freya From 1debf488ee1ed24763a01c2e1bf5c3f4119de18f Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 10 Nov 2025 10:00:04 -0500 Subject: controlcenter: ethernet panel (debug) --- modules/controlcenter/NavRail.qml | 9 +- modules/controlcenter/Panes.qml | 14 +- modules/controlcenter/Session.qml | 7 +- modules/controlcenter/ethernet/EthernetDetails.qml | 204 ++++++++++++++++ modules/controlcenter/ethernet/EthernetList.qml | 262 +++++++++++++++++++++ modules/controlcenter/ethernet/EthernetPane.qml | 156 ++++++++++++ .../controlcenter/ethernet/EthernetSettings.qml | 155 ++++++++++++ services/Network.qml | 183 +++++++++++++- 8 files changed, 983 insertions(+), 7 deletions(-) create mode 100644 modules/controlcenter/ethernet/EthernetDetails.qml create mode 100644 modules/controlcenter/ethernet/EthernetList.qml create mode 100644 modules/controlcenter/ethernet/EthernetPane.qml create mode 100644 modules/controlcenter/ethernet/EthernetSettings.qml diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 96bbb65..b4fbf94 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -158,8 +158,13 @@ Item { NavItem { Layout.topMargin: Appearance.spacing.large * 2 - icon: "network_manage" - label: "network" + icon: "cable" + label: "ethernet" + } + + NavItem { + icon: "wifi" + label: "wireless" } NavItem { diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 5b1039c..4f4a337 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -1,5 +1,6 @@ pragma ComponentBehavior: Bound +import "ethernet" import "bluetooth" import "network" import "audio" @@ -26,27 +27,34 @@ ClippingRectangle { Pane { index: 0 - sourceComponent: NetworkPane { + sourceComponent: EthernetPane { session: root.session } } Pane { index: 1 - sourceComponent: BtPane { + sourceComponent: NetworkPane { session: root.session } } Pane { index: 2 - sourceComponent: AudioPane { + sourceComponent: BtPane { session: root.session } } Pane { index: 3 + sourceComponent: AudioPane { + session: root.session + } + } + + Pane { + index: 4 sourceComponent: AppearancePane { session: root.session } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index 4ac09a4..f7c07e4 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -2,7 +2,7 @@ import Quickshell.Bluetooth import QtQuick QtObject { - readonly property list panes: ["network", "bluetooth", "audio", "appearance"] + readonly property list panes: ["ethernet", "wireless", "bluetooth", "audio", "appearance"] required property var root property bool floating: false @@ -12,6 +12,7 @@ QtObject { readonly property Bt bt: Bt {} readonly property Network network: Network {} + readonly property Ethernet ethernet: Ethernet {} onActiveChanged: activeIndex = panes.indexOf(active) onActiveIndexChanged: active = panes[activeIndex] @@ -29,4 +30,8 @@ QtObject { property bool showPasswordDialog: false property var pendingNetwork } + + component Ethernet: QtObject { + property var active + } } diff --git a/modules/controlcenter/ethernet/EthernetDetails.qml b/modules/controlcenter/ethernet/EthernetDetails.qml new file mode 100644 index 0000000..9be3ddc --- /dev/null +++ b/modules/controlcenter/ethernet/EthernetDetails.qml @@ -0,0 +1,204 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Session session + readonly property var device: session.ethernet.active + + StyledFlickable { + anchors.fill: parent + + flickableDirection: Flickable.VerticalFlick + contentHeight: layout.height + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + animate: true + text: "cable" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + animate: true + text: root.device?.interface ?? qsTr("Unknown") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Connection status") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Connection settings for this device") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: deviceStatus + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.larger + + Toggle { + label: qsTr("Connected") + checked: root.device?.connected ?? false + toggle.onToggled: { + if (checked) { + if (root.device?.connection) { + Network.connectEthernet(root.device.connection); + } + } else { + if (root.device?.connection) { + Network.disconnectEthernet(root.device.connection); + } + } + } + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Device properties") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Additional information") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: deviceProps + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("Interface") + } + + StyledText { + text: root.device?.interface ?? qsTr("Unknown") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Connection") + } + + StyledText { + text: root.device?.connection || qsTr("Not connected") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("State") + } + + StyledText { + text: root.device?.state ?? qsTr("Unknown") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + } + } + + } + } + + component Toggle: RowLayout { + required property string label + property alias checked: toggle.checked + property alias toggle: toggle + + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: parent.label + } + + StyledSwitch { + id: toggle + + cLayer: 2 + } + } +} + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/controlcenter/ethernet/EthernetList.qml b/modules/controlcenter/ethernet/EthernetList.qml new file mode 100644 index 0000000..d239fc6 --- /dev/null +++ b/modules/controlcenter/ethernet/EthernetList.qml @@ -0,0 +1,262 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + ToggleButton { + toggled: !root.session.ethernet.active + icon: "settings" + accent: "Primary" + + function onClicked(): void { + if (root.session.ethernet.active) + root.session.ethernet.active = null; + else { + root.session.ethernet.active = view.model.get(0)?.modelData ?? null; + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Devices (%1)").arg(Network.ethernetDeviceCount || Network.ethernetDevices.length) + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + } + + StyledText { + text: qsTr("All available ethernet devices") + color: Colours.palette.m3outline + } + + StyledListView { + id: view + + Layout.fillWidth: true + Layout.fillHeight: true + + model: Network.ethernetDevices + + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: view + } + + delegate: StyledRect { + required property var modelData + + anchors.left: parent.left + anchors.right: parent.right + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: root.session.ethernet.active === modelData ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + root.session.ethernet.active = modelData; + } + } + + RowLayout { + id: rowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: "cable" + font.pointSize: Appearance.font.size.large + fill: modelData.connected ? 1 : 0 + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + + StyledText { + Layout.fillWidth: true + + text: modelData.interface || qsTr("Unknown") + } + + StyledText { + text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.connected ? 500 : 400 + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) + + StateLayer { + function onClicked(): void { + if (modelData.connected && modelData.connection) { + Network.disconnectEthernet(modelData.connection); + } else if (modelData.connection) { + Network.connectEthernet(modelData.connection); + } + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + text: modelData.connected ? "link_off" : "link" + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + } + + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + + component ToggleButton: StyledRect { + id: toggleBtn + + required property bool toggled + property string icon + property string label + property string accent: "Secondary" + + function onClicked(): void { + } + + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) + implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2 + implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) + color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] + + StateLayer { + id: toggleStateLayer + + color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] + + function onClicked(): void { + toggleBtn.onClicked(); + } + } + + RowLayout { + id: toggleBtnInner + + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + id: toggleBtnIcon + + visible: !!text + fill: toggleBtn.toggled ? 1 : 0 + text: toggleBtn.icon + color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] + font.pointSize: Appearance.font.size.large + + Behavior on fill { + Anim {} + } + } + + Loader { + asynchronous: true + active: !!toggleBtn.label + visible: active + + sourceComponent: StyledText { + text: toggleBtn.label + color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] + } + } + } + + Behavior on radius { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + Behavior on Layout.preferredWidth { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } +} + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/controlcenter/ethernet/EthernetPane.qml b/modules/controlcenter/ethernet/EthernetPane.qml new file mode 100644 index 0000000..fc3e1c0 --- /dev/null +++ b/modules/controlcenter/ethernet/EthernetPane.qml @@ -0,0 +1,156 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.effects +import qs.components.containers +import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property Session session + + anchors.fill: parent + + spacing: 0 + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true + + EthernetList { + 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 + + session: root.session + } + + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ClippingRectangle { + 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 var pane: root.session.ethernet.active + property string paneId: pane ? (pane.interface || "") : "" + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + + clip: false + asynchronous: true + sourceComponent: pane ? details : settings + + 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 + } + } + PropertyAction {} + 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 + } + } + } + } + + onPaneChanged: { + paneId = pane ? (pane.interface || "") : ""; + } + } + } + + InnerBorder { + id: rightBorder + + leftThickness: Appearance.padding.normal / 2 + } + + Component { + id: settings + + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + + EthernetSettings { + id: settingsInner + + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } + } + } + + Component { + id: details + + EthernetDetails { + session: root.session + } + } + } + + component Anim: NumberAnimation { + target: loader + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + } +} + + + + + + + + diff --git a/modules/controlcenter/ethernet/EthernetSettings.qml b/modules/controlcenter/ethernet/EthernetSettings.qml new file mode 100644 index 0000000..b780b55 --- /dev/null +++ b/modules/controlcenter/ethernet/EthernetSettings.qml @@ -0,0 +1,155 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "cable" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Ethernet settings") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Ethernet devices") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Available ethernet devices") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: ethernetInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("Total devices") + } + + StyledText { + text: qsTr("%1").arg(Network.ethernetDeviceCount || Network.ethernetDevices.length) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Connected devices") + } + + StyledText { + text: qsTr("%1").arg(Network.ethernetDevices.filter(d => d.connected).length) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Debug Info") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: debugInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: debugInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("Process running: %1").arg(Network.ethernetProcessRunning ? "Yes" : "No") + font.pointSize: Appearance.font.size.small + } + + StyledText { + text: qsTr("List length: %1").arg(Network.ethernetDevices.length) + font.pointSize: Appearance.font.size.small + } + + StyledText { + text: qsTr("Device count: %1").arg(Network.ethernetDeviceCount) + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Debug: %1").arg(Network.ethernetDebugInfo || "No info") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline + wrapMode: Text.Wrap + Layout.maximumWidth: parent.width + } + } + } +} + + + + + + + + + + + + + + + + + + + + + diff --git a/services/Network.qml b/services/Network.qml index 3ceadab..1dee367 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -7,11 +7,24 @@ import QtQuick Singleton { id: root + Component.onCompleted: { + // Trigger ethernet device detection after initialization + Qt.callLater(() => { + getEthernetDevices(); + }); + } + readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property bool wifiEnabled: true readonly property bool scanning: rescanProc.running + property list ethernetDevices: [] + readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null + property int ethernetDeviceCount: 0 + property string ethernetDebugInfo: "" + property bool ethernetProcessRunning: false + function enableWifi(enabled: bool): void { const cmd = enabled ? "on" : "off"; enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]); @@ -70,11 +83,27 @@ Singleton { wifiStatusProc.running = true; } + function getEthernetDevices(): void { + getEthernetDevicesProc.running = true; + } + + + function connectEthernet(connectionName: string): void { + connectEthernetProc.exec(["nmcli", "connection", "up", connectionName]); + } + + function disconnectEthernet(connectionName: string): void { + disconnectEthernetProc.exec(["nmcli", "connection", "down", connectionName]); + } + Process { running: true command: ["nmcli", "m"] stdout: SplitParser { - onRead: getNetworks.running = true + onRead: { + getNetworks.running = true; + getEthernetDevices(); + } } } @@ -336,6 +365,158 @@ Singleton { } } + Process { + id: getEthernetDevicesProc + + running: false + command: ["nmcli", "-g", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + onRunningChanged: { + root.ethernetProcessRunning = running; + if (!running) { + // Process finished, update debug info + Qt.callLater(() => { + if (root.ethernetDebugInfo === "" || root.ethernetDebugInfo.includes("Process exited")) { + root.ethernetDebugInfo = "Process finished, waiting for output..."; + } + }); + } + } + onExited: { + Qt.callLater(() => { + const outputLength = ethernetStdout.text ? ethernetStdout.text.length : 0; + root.ethernetDebugInfo = "Process exited with code: " + exitCode + ", output length: " + outputLength; + if (outputLength > 0) { + // Output was captured, process it + const output = ethernetStdout.text.trim(); + root.ethernetDebugInfo = "Processing output from onExited, length: " + output.length + "\nOutput: " + output.substring(0, 200); + root.processEthernetOutput(output); + } else { + root.ethernetDebugInfo = "No output captured in onExited"; + } + }); + } + stdout: StdioCollector { + id: ethernetStdout + onStreamFinished: { + const output = text.trim(); + root.ethernetDebugInfo = "Output received in onStreamFinished! Length: " + output.length + ", First 100 chars: " + output.substring(0, 100); + + if (!output || output.length === 0) { + root.ethernetDebugInfo = "No output received (empty)"; + return; + } + + root.processEthernetOutput(output); + } + } + } + + function processEthernetOutput(output: string): void { + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; + const rep = new RegExp("\\\\:", "g"); + const rep2 = new RegExp(PLACEHOLDER, "g"); + + const lines = output.split("\n"); + root.ethernetDebugInfo = "Processing " + lines.length + " lines"; + + const allDevices = lines.map(d => { + const dev = d.replace(rep, PLACEHOLDER).split(":"); + return { + interface: dev[0]?.replace(rep2, ":") ?? "", + type: dev[1]?.replace(rep2, ":") ?? "", + state: dev[2]?.replace(rep2, ":") ?? "", + connection: dev[3]?.replace(rep2, ":") ?? "" + }; + }); + + root.ethernetDebugInfo = "All devices: " + allDevices.length + ", Types: " + allDevices.map(d => d.type).join(", "); + + const ethernetOnly = allDevices.filter(d => d.type === "ethernet"); + root.ethernetDebugInfo = "Ethernet devices found: " + ethernetOnly.length; + + const ethernetDevices = ethernetOnly.map(d => { + const state = d.state || ""; + const connected = state === "100 (connected)" || state === "connected" || state.startsWith("connected"); + return { + interface: d.interface, + type: d.type, + state: state, + connection: d.connection, + connected: connected, + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }; + }); + + root.ethernetDebugInfo = "Ethernet devices processed: " + ethernetDevices.length + ", First device: " + (ethernetDevices[0]?.interface || "none"); + + // Update the list - replace the entire array to ensure QML detects the change + // Create a new array and assign it to the property + const newDevices = []; + for (let i = 0; i < ethernetDevices.length; i++) { + newDevices.push(ethernetDevices[i]); + } + + // Replace the entire list + root.ethernetDevices = newDevices; + + // Force QML to detect the change by updating a property + root.ethernetDeviceCount = ethernetDevices.length; + + // Force QML to re-evaluate the list by accessing it + Qt.callLater(() => { + const count = root.ethernetDevices.length; + root.ethernetDebugInfo = "Final: Found " + ethernetDevices.length + " devices, List length: " + count + ", Parsed all: " + allDevices.length + ", Output length: " + output.length; + }); + } + + + Process { + id: connectEthernetProc + + onExited: { + getEthernetDevices(); + } + stdout: SplitParser { + onRead: getEthernetDevices() + } + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0 && !error.includes("successfully") && !error.includes("Connection activated")) { + console.warn("Ethernet connection error:", error); + } + } + } + } + + Process { + id: disconnectEthernetProc + + onExited: { + getEthernetDevices(); + } + stdout: SplitParser { + onRead: getEthernetDevices() + } + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { + console.warn("Ethernet disconnection error:", error); + } + } + } + } + component AccessPoint: QtObject { required property var lastIpcObject readonly property string ssid: lastIpcObject.ssid -- cgit v1.2.3-freya From 817156aec079852141d52d484dd14eec3fa0a88e Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 10 Nov 2025 11:51:43 -0500 Subject: controlcenter: polished ethernet panel --- modules/controlcenter/ethernet/EthernetDetails.qml | 95 ++++++++++++++++- modules/controlcenter/ethernet/EthernetList.qml | 5 +- .../controlcenter/ethernet/EthernetSettings.qml | 50 --------- services/Network.qml | 112 ++++++++++++++++++++- 4 files changed, 205 insertions(+), 57 deletions(-) diff --git a/modules/controlcenter/ethernet/EthernetDetails.qml b/modules/controlcenter/ethernet/EthernetDetails.qml index 9be3ddc..1db3db0 100644 --- a/modules/controlcenter/ethernet/EthernetDetails.qml +++ b/modules/controlcenter/ethernet/EthernetDetails.qml @@ -16,6 +16,20 @@ Item { required property Session session readonly property var device: session.ethernet.active + Component.onCompleted: { + if (device && device.interface) { + Network.updateEthernetDeviceDetails(device.interface); + } + } + + onDeviceChanged: { + if (device && device.interface) { + Network.updateEthernetDeviceDetails(device.interface); + } else { + Network.ethernetDeviceDetails = null; + } + } + StyledFlickable { anchors.fill: parent @@ -79,9 +93,8 @@ Item { checked: root.device?.connected ?? false toggle.onToggled: { if (checked) { - if (root.device?.connection) { - Network.connectEthernet(root.device.connection); - } + // Use connection name if available, otherwise use interface + Network.connectEthernet(root.device?.connection || "", root.device?.interface || ""); } else { if (root.device?.connection) { Network.disconnectEthernet(root.device.connection); @@ -155,6 +168,82 @@ Item { } } + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Connection information") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Network connection details") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: connectionInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: connectionInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("IP Address") + } + + StyledText { + text: Network.ethernetDeviceDetails?.ipAddress || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Subnet Mask") + } + + StyledText { + text: Network.ethernetDeviceDetails?.subnet || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Gateway") + } + + StyledText { + text: Network.ethernetDeviceDetails?.gateway || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("DNS Servers") + } + + StyledText { + text: (Network.ethernetDeviceDetails && Network.ethernetDeviceDetails.dns && Network.ethernetDeviceDetails.dns.length > 0) ? Network.ethernetDeviceDetails.dns.join(", ") : qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + wrapMode: Text.Wrap + Layout.maximumWidth: parent.width + } + } + } + } } diff --git a/modules/controlcenter/ethernet/EthernetList.qml b/modules/controlcenter/ethernet/EthernetList.qml index d239fc6..6ed50fd 100644 --- a/modules/controlcenter/ethernet/EthernetList.qml +++ b/modules/controlcenter/ethernet/EthernetList.qml @@ -144,8 +144,9 @@ ColumnLayout { function onClicked(): void { if (modelData.connected && modelData.connection) { Network.disconnectEthernet(modelData.connection); - } else if (modelData.connection) { - Network.connectEthernet(modelData.connection); + } else { + // Use connection name if available, otherwise use interface + Network.connectEthernet(modelData.connection || "", modelData.interface || ""); } } } diff --git a/modules/controlcenter/ethernet/EthernetSettings.qml b/modules/controlcenter/ethernet/EthernetSettings.qml index b780b55..33b1449 100644 --- a/modules/controlcenter/ethernet/EthernetSettings.qml +++ b/modules/controlcenter/ethernet/EthernetSettings.qml @@ -81,56 +81,6 @@ ColumnLayout { } } } - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Debug Info") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: debugInfo.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: debugInfo - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.small / 2 - - StyledText { - text: qsTr("Process running: %1").arg(Network.ethernetProcessRunning ? "Yes" : "No") - font.pointSize: Appearance.font.size.small - } - - StyledText { - text: qsTr("List length: %1").arg(Network.ethernetDevices.length) - font.pointSize: Appearance.font.size.small - } - - StyledText { - text: qsTr("Device count: %1").arg(Network.ethernetDeviceCount) - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Debug: %1").arg(Network.ethernetDebugInfo || "No info") - font.pointSize: Appearance.font.size.small - color: Colours.palette.m3outline - wrapMode: Text.Wrap - Layout.maximumWidth: parent.width - } - } - } } diff --git a/services/Network.qml b/services/Network.qml index 1dee367..acd4bcb 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -24,6 +24,7 @@ Singleton { property int ethernetDeviceCount: 0 property string ethernetDebugInfo: "" property bool ethernetProcessRunning: false + property var ethernetDeviceDetails: null function enableWifi(enabled: bool): void { const cmd = enabled ? "on" : "off"; @@ -88,14 +89,46 @@ Singleton { } - function connectEthernet(connectionName: string): void { - connectEthernetProc.exec(["nmcli", "connection", "up", connectionName]); + function connectEthernet(connectionName: string, interfaceName: string): void { + if (connectionName && connectionName.length > 0) { + // Use connection name if available + connectEthernetProc.exec(["nmcli", "connection", "up", connectionName]); + } else if (interfaceName && interfaceName.length > 0) { + // Fallback to device interface if no connection name + connectEthernetProc.exec(["nmcli", "device", "connect", interfaceName]); + } } function disconnectEthernet(connectionName: string): void { disconnectEthernetProc.exec(["nmcli", "connection", "down", connectionName]); } + function updateEthernetDeviceDetails(interfaceName: string): void { + if (interfaceName && interfaceName.length > 0) { + getEthernetDetailsProc.exec(["nmcli", "device", "show", interfaceName]); + } else { + ethernetDeviceDetails = null; + } + } + + function cidrToSubnetMask(cidr: string): string { + // Convert CIDR notation (e.g., "24") to subnet mask (e.g., "255.255.255.0") + const cidrNum = parseInt(cidr); + if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { + return ""; + } + + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; + const octets = [ + (mask >>> 24) & 0xff, + (mask >>> 16) & 0xff, + (mask >>> 8) & 0xff, + mask & 0xff + ]; + + return octets.join("."); + } + Process { running: true command: ["nmcli", "m"] @@ -484,6 +517,13 @@ Singleton { onExited: { getEthernetDevices(); + // Refresh device details after connection + Qt.callLater(() => { + const activeDevice = root.ethernetDevices.find(function(d) { return d.connected; }); + if (activeDevice && activeDevice.interface) { + updateEthernetDeviceDetails(activeDevice.interface); + } + }); } stdout: SplitParser { onRead: getEthernetDevices() @@ -503,6 +543,10 @@ Singleton { onExited: { getEthernetDevices(); + // Clear device details after disconnection + Qt.callLater(() => { + root.ethernetDeviceDetails = null; + }); } stdout: SplitParser { onRead: getEthernetDevices() @@ -517,6 +561,70 @@ Singleton { } } + Process { + id: getEthernetDetailsProc + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: StdioCollector { + onStreamFinished: { + const output = text.trim(); + if (!output || output.length === 0) { + root.ethernetDeviceDetails = null; + return; + } + + const lines = output.split("\n"); + const details = { + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(":"); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join(":").trim(); + + if (key.startsWith("IP4.ADDRESS")) { + // Extract IP and subnet from format like "10.13.1.45/24" + const ipParts = value.split("/"); + details.ipAddress = ipParts[0] || ""; + if (ipParts[1]) { + // Convert CIDR notation to subnet mask + details.subnet = root.cidrToSubnetMask(ipParts[1]); + } else { + details.subnet = ""; + } + } else if (key === "IP4.GATEWAY") { + details.gateway = value; + } else if (key.startsWith("IP4.DNS")) { + details.dns.push(value); + } else if (key === "WIRED-PROPERTIES.MAC") { + details.macAddress = value; + } else if (key === "WIRED-PROPERTIES.SPEED") { + details.speed = value; + } + } + } + + root.ethernetDeviceDetails = details; + } + } + onExited: { + if (exitCode !== 0) { + root.ethernetDeviceDetails = null; + } + } + } + component AccessPoint: QtObject { required property var lastIpcObject readonly property string ssid: lastIpcObject.ssid -- cgit v1.2.3-freya From f5ca27c9674d7b205e83b23589b03eccf9379b83 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 10 Nov 2025 12:05:02 -0500 Subject: controlcenter: polishing appearance panel --- .../controlcenter/appearance/AppearancePane.qml | 81 ++++++++++++---------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index b2692c8..c8a4522 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -108,6 +108,7 @@ RowLayout { StyledListView { Layout.fillWidth: true Layout.fillHeight: true + Layout.topMargin: 0 model: M3Variants.list spacing: Appearance.spacing.small / 2 @@ -202,6 +203,7 @@ RowLayout { StyledListView { Layout.fillWidth: true Layout.fillHeight: true + Layout.topMargin: 0 model: Schemes.list spacing: Appearance.spacing.small / 2 @@ -262,66 +264,75 @@ RowLayout { spacing: Appearance.spacing.normal - Item { - readonly property real itemHeight: schemeRow.implicitHeight || 50 - Layout.preferredWidth: itemHeight * 0.8 - Layout.preferredHeight: itemHeight * 0.8 + StyledRect { + id: preview - StyledRect { - id: preview + anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenter: parent.verticalCenter + border.width: 1 + border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) - border.width: 1 - border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) + color: `#${modelData.colours?.surface}` + radius: Appearance.rounding.full + implicitWidth: iconPlaceholder.implicitWidth + implicitHeight: iconPlaceholder.implicitWidth - color: `#${modelData.colours?.surface}` - radius: Appearance.rounding.full - implicitWidth: parent.itemHeight * 0.8 - implicitHeight: parent.itemHeight * 0.8 + MaterialIcon { + id: iconPlaceholder + visible: false + text: "circle" + font.pointSize: Appearance.font.size.large + } + + Item { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + implicitWidth: parent.implicitWidth / 2 + clip: true - Item { + StyledRect { anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right - implicitWidth: parent.implicitWidth / 2 - clip: true - - StyledRect { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - - implicitWidth: preview.implicitWidth - color: `#${modelData.colours?.primary}` - radius: Appearance.rounding.full - } + implicitWidth: preview.implicitWidth + color: `#${modelData.colours?.primary}` + radius: Appearance.rounding.full } } } - ColumnLayout { + Column { Layout.fillWidth: true spacing: 0 StyledText { - text: modelData.name - font.weight: isCurrent ? 500 : 400 + text: modelData.flavour ?? "" + font.pointSize: Appearance.font.size.normal } StyledText { - text: modelData.flavour + text: modelData.name ?? "" font.pointSize: Appearance.font.size.small color: Colours.palette.m3outline + + elide: Text.ElideRight + anchors.left: parent.left + anchors.right: parent.right } } - MaterialIcon { - visible: isCurrent - text: "check" - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } -- cgit v1.2.3-freya From bd61cbaac5ef765696bfa786dabff977ab6d03e9 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 10 Nov 2025 14:25:06 -0500 Subject: statusicons: added ethernet when active and popout --- config/BarConfig.qml | 1 + modules/bar/components/StatusIcons.qml | 12 +++ modules/bar/popouts/Content.qml | 5 + modules/bar/popouts/Ethernet.qml | 179 +++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+) create mode 100644 modules/bar/popouts/Ethernet.qml diff --git a/config/BarConfig.qml b/config/BarConfig.qml index 34819b1..b81d7e9 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -90,6 +90,7 @@ JsonObject { property bool showMicrophone: false property bool showKbLayout: false property bool showNetwork: true + property bool showEthernet: true property bool showBluetooth: true property bool showBattery: true property bool showLockStatus: true diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 27fd9f8..6113539 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -152,6 +152,18 @@ StyledRect { } } + // Ethernet icon + WrappedLoader { + name: "ethernet" + active: Config.bar.status.showEthernet && Network.activeEthernet + + sourceComponent: MaterialIcon { + animate: true + text: "cable" + color: root.colour + } + } + // Bluetooth section WrappedLoader { Layout.preferredHeight: implicitHeight diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index e3f569d..f8ef549 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -36,6 +36,11 @@ Item { sourceComponent: Network {} } + Popout { + name: "ethernet" + sourceComponent: Ethernet {} + } + Popout { name: "bluetooth" sourceComponent: Bluetooth { diff --git a/modules/bar/popouts/Ethernet.qml b/modules/bar/popouts/Ethernet.qml new file mode 100644 index 0000000..b4a753c --- /dev/null +++ b/modules/bar/popouts/Ethernet.qml @@ -0,0 +1,179 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.controls +import qs.services +import qs.config +import qs.utils +import Quickshell +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + spacing: Appearance.spacing.small + width: Config.bar.sizes.networkWidth + + StyledText { + Layout.topMargin: Appearance.padding.normal + Layout.rightMargin: Appearance.padding.small + text: qsTr("Ethernet") + font.weight: 500 + } + + StyledText { + Layout.topMargin: Appearance.spacing.small + Layout.rightMargin: Appearance.padding.small + text: qsTr("%1 devices available").arg(Network.ethernetDeviceCount || Network.ethernetDevices.length) + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + } + + Repeater { + model: ScriptModel { + values: [...Network.ethernetDevices].sort((a, b) => { + if (a.connected !== b.connected) + return b.connected - a.connected; + return (a.interface || "").localeCompare(b.interface || ""); + }).slice(0, 8) + } + + RowLayout { + id: ethernetItem + + required property var modelData + readonly property bool loading: false + + Layout.fillWidth: true + Layout.rightMargin: Appearance.padding.small + spacing: Appearance.spacing.small + + opacity: 0 + scale: 0.7 + + Component.onCompleted: { + opacity = 1; + scale = 1; + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + MaterialIcon { + text: "cable" + color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + StyledText { + Layout.leftMargin: Appearance.spacing.small / 2 + Layout.rightMargin: Appearance.spacing.small / 2 + Layout.fillWidth: true + text: ethernetItem.modelData.interface || qsTr("Unknown") + elide: Text.ElideRight + font.weight: ethernetItem.modelData.connected ? 500 : 400 + color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface + } + + StyledRect { + id: connectBtn + + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.small + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0) + + CircularIndicator { + anchors.fill: parent + running: ethernetItem.loading + } + + StateLayer { + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: ethernetItem.loading + + function onClicked(): void { + if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { + Network.disconnectEthernet(ethernetItem.modelData.connection); + } else { + Network.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || ""); + } + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + animate: true + text: ethernetItem.modelData.connected ? "link_off" : "link" + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + + opacity: ethernetItem.loading ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + } + } + } + + StyledRect { + Layout.topMargin: Appearance.spacing.small + Layout.fillWidth: true + implicitHeight: refreshBtn.implicitHeight + Appearance.padding.small * 2 + + radius: Appearance.rounding.full + color: Colours.palette.m3primaryContainer + + StateLayer { + color: Colours.palette.m3onPrimaryContainer + disabled: Network.ethernetProcessRunning + + function onClicked(): void { + Network.getEthernetDevices(); + } + } + + RowLayout { + id: refreshBtn + + anchors.centerIn: parent + spacing: Appearance.spacing.small + opacity: Network.ethernetProcessRunning ? 0 : 1 + + MaterialIcon { + id: refreshIcon + + animate: true + text: "refresh" + color: Colours.palette.m3onPrimaryContainer + } + + StyledText { + text: qsTr("Refresh devices") + color: Colours.palette.m3onPrimaryContainer + } + + Behavior on opacity { + Anim {} + } + } + + CircularIndicator { + anchors.centerIn: parent + strokeWidth: Appearance.padding.small / 2 + bgColour: "transparent" + implicitHeight: parent.implicitHeight - Appearance.padding.smaller * 2 + running: Network.ethernetProcessRunning + } + } +} + -- cgit v1.2.3-freya From 16ab93f637337f320a9095b54e2f90ae813acdb6 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 10 Nov 2025 14:28:48 -0500 Subject: statusicons: sound input/output now opens controlcenter instead of pavucontrol --- modules/bar/popouts/Audio.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 952dd6b..5803f1b 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -4,6 +4,7 @@ import qs.components import qs.components.controls import qs.services import qs.config +import qs.modules.controlcenter import Quickshell import Quickshell.Services.Pipewire import QtQuick @@ -106,7 +107,6 @@ Item { StyledRect { Layout.topMargin: Appearance.spacing.normal - visible: Config.general.apps.audio.length > 0 implicitWidth: expandBtn.implicitWidth + Appearance.padding.normal * 2 implicitHeight: expandBtn.implicitHeight + Appearance.padding.small @@ -119,7 +119,7 @@ Item { function onClicked(): void { root.wrapper.hasCurrent = false; - Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.audio]); + WindowFactory.create(null, { active: "audio" }); } } -- cgit v1.2.3-freya From a48bb8a0a38f637f61b971e74c40649cbe0ccd63 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 10 Nov 2025 14:31:12 -0500 Subject: statusicons: made panel open in detach to match the bluetooth icon --- modules/bar/popouts/Audio.qml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 5803f1b..55a30a2 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -4,7 +4,6 @@ import qs.components import qs.components.controls import qs.services import qs.config -import qs.modules.controlcenter import Quickshell import Quickshell.Services.Pipewire import QtQuick @@ -118,8 +117,7 @@ Item { color: Colours.palette.m3onPrimaryContainer function onClicked(): void { - root.wrapper.hasCurrent = false; - WindowFactory.create(null, { active: "audio" }); + root.wrapper.detach("audio"); } } @@ -131,7 +129,7 @@ Item { StyledText { Layout.leftMargin: Appearance.padding.smaller - text: qsTr("Open settings") + text: qsTr("Open panel") color: Colours.palette.m3onPrimaryContainer } -- cgit v1.2.3-freya From 7ad940ea9f651187118048179689dfdbbdc71427 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 10 Nov 2025 15:57:11 -0500 Subject: controlcenter: taskbar object toggles --- modules/controlcenter/NavRail.qml | 5 + modules/controlcenter/Panes.qml | 8 + modules/controlcenter/Session.qml | 2 +- modules/controlcenter/taskbar/TaskbarPane.qml | 402 ++++++++++++++++++++++++++ 4 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 modules/controlcenter/taskbar/TaskbarPane.qml diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index b4fbf94..1f8c6b3 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -181,6 +181,11 @@ Item { icon: "palette" label: "appearance" } + + NavItem { + icon: "task_alt" + label: "taskbar" + } } component NavItem: Item { diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 4f4a337..b30bcf2 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -5,6 +5,7 @@ import "bluetooth" import "network" import "audio" import "appearance" +import "taskbar" import qs.components import qs.services import qs.config @@ -60,6 +61,13 @@ ClippingRectangle { } } + Pane { + index: 5 + sourceComponent: TaskbarPane { + session: root.session + } + } + Behavior on y { Anim {} } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index f7c07e4..cf31ec3 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -2,7 +2,7 @@ import Quickshell.Bluetooth import QtQuick QtObject { - readonly property list panes: ["ethernet", "wireless", "bluetooth", "audio", "appearance"] + readonly property list panes: ["ethernet", "wireless", "bluetooth", "audio", "appearance", "taskbar"] required property var root property bool floating: false diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml new file mode 100644 index 0000000..a367635 --- /dev/null +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -0,0 +1,402 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import qs.utils +import Quickshell +import Quickshell.Io +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property Session session + + anchors.fill: parent + + spacing: 0 + + FileView { + id: configFile + + path: `${Paths.config}/shell.json` + watchChanges: true + + onLoaded: { + try { + const config = JSON.parse(text()); + updateFromConfig(config); + } catch (e) { + console.error("Failed to parse config:", e); + } + } + } + + function updateFromConfig(config) { + // Update clock toggle + if (config.bar && config.bar.clock) { + clockShowIconSwitch.checked = config.bar.clock.showIcon !== false; + } + + // Update entries + if (config.bar && config.bar.entries) { + entriesModel.clear(); + for (const entry of config.bar.entries) { + entriesModel.append({ + id: entry.id, + enabled: entry.enabled !== false + }); + } + } + } + + function saveConfig(entryIndex, entryEnabled) { + if (!configFile.loaded) { + root.lastSaveStatus = "Error: Config file not loaded yet"; + root.debugInfo = "Config file not loaded yet, cannot save"; + return; + } + + try { + const config = JSON.parse(configFile.text()); + + // Update clock setting (same simple approach - read directly from the switch) + if (!config.bar) config.bar = {}; + if (!config.bar.clock) config.bar.clock = {}; + config.bar.clock.showIcon = clockShowIconSwitch.checked; + + // Update entries from the model (same approach as clock - use provided value if available) + if (!config.bar.entries) config.bar.entries = []; + config.bar.entries = []; + + let debugInfo = `saveConfig called\n`; + debugInfo += `entryIndex: ${entryIndex}\n`; + debugInfo += `entryEnabled: ${entryEnabled}\n`; + debugInfo += `entriesModel.count: ${entriesModel.count}\n\n`; + + for (let i = 0; i < entriesModel.count; i++) { + const entry = entriesModel.get(i); + // If this is the entry being updated, use the provided value (same as clock toggle reads from switch) + // Otherwise use the value from the model + let enabled = entry.enabled; + if (entryIndex !== undefined && i === entryIndex) { + enabled = entryEnabled; + debugInfo += `Entry ${i} (${entry.id}): Using provided value = ${entryEnabled}\n`; + } else { + debugInfo += `Entry ${i} (${entry.id}): Using model value = ${entry.enabled}\n`; + } + config.bar.entries.push({ + id: entry.id, + enabled: enabled + }); + } + + debugInfo += `\nFinal entries array:\n${JSON.stringify(config.bar.entries, null, 2)}\n`; + debugInfo += `\nFinal clock.showIcon: ${config.bar.clock.showIcon}\n`; + root.debugInfo = debugInfo; + + // Write back to file using setText (same simple approach that worked for clock) + const jsonString = JSON.stringify(config, null, 4); + configFile.setText(jsonString); + root.lastSaveStatus = `Saved! Entries count: ${config.bar.entries.length}`; + } catch (e) { + root.lastSaveStatus = `Error: ${e.message}`; + root.debugInfo = `Failed to save config:\n${e.message}\n${e.stack}`; + } + } + + ListModel { + id: entriesModel + } + + // Debug info + property string debugInfo: "" + property string lastSaveStatus: "" + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true + + ColumnLayout { + 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 + + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Clock") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Clock display settings") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: clockRow.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + RowLayout { + id: clockRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show clock icon") + } + + StyledSwitch { + id: clockShowIconSwitch + checked: true + onToggled: { + root.saveConfig(); + } + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Taskbar Entries") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Enable or disable taskbar entries") + color: Colours.palette.m3outline + } + + StyledListView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: 0 + + model: entriesModel + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + delegate: StyledRect { + id: delegate + required property string id + required property bool enabled + required property int index + + anchors.left: parent.left + anchors.right: parent.right + + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal + + RowLayout { + id: entryRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: id.charAt(0).toUpperCase() + id.slice(1) + font.weight: enabled ? 500 : 400 + } + + StyledSwitch { + checked: enabled + onToggled: { + // Store the values in local variables to ensure they're accessible + // Access index from the delegate + const entryIndex = delegate.index; + const entryEnabled = checked; + console.log(`Entry toggle: index=${entryIndex}, checked=${entryEnabled}`); + // Update the model first + entriesModel.setProperty(entryIndex, "enabled", entryEnabled); + // Save immediately with the value directly (same technique as clock toggle) + // Clock toggle reads directly from clockShowIconSwitch.checked + // We pass the value directly here (same approach) + root.saveConfig(entryIndex, entryEnabled); + } + } + } + + implicitHeight: entryRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } + + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + StyledFlickable { + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.implicitHeight + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + ColumnLayout { + id: contentLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "task_alt" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Taskbar settings") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + Layout.alignment: Qt.AlignHCenter + text: qsTr("Clock") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: clockShowIconSwitch.checked ? qsTr("Clock icon enabled") : qsTr("Clock icon disabled") + color: Colours.palette.m3outline + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + Layout.alignment: Qt.AlignHCenter + text: qsTr("Taskbar Entries") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Configure which entries appear in the taskbar") + color: Colours.palette.m3outline + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + Layout.alignment: Qt.AlignHCenter + text: qsTr("Debug Info") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledRect { + Layout.fillWidth: true + Layout.preferredHeight: 200 + Layout.maximumHeight: 300 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + clip: true + + StyledFlickable { + id: debugFlickable + anchors.fill: parent + anchors.margins: Appearance.padding.normal + contentHeight: debugText.implicitHeight + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: debugFlickable + } + + TextEdit { + id: debugText + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + width: parent.width + + text: root.debugInfo || "No debug info yet" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurface + wrapMode: TextEdit.Wrap + readOnly: true + selectByMouse: true + selectByKeyboard: true + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.small + Layout.alignment: Qt.AlignHCenter + text: root.lastSaveStatus || "" + font.pointSize: Appearance.font.size.small + color: root.lastSaveStatus.includes("Error") ? Colours.palette.m3error : Colours.palette.m3primary + } + } + } + + InnerBorder { + leftThickness: Appearance.padding.normal / 2 + } + } +} + -- cgit v1.2.3-freya From 97d461871a5fbfedb3aff080303145efb8a6fffc Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 11:41:30 -0500 Subject: controlcenter: launcher panel, can hide/show applications --- modules/controlcenter/NavRail.qml | 5 + modules/controlcenter/Panes.qml | 8 + modules/controlcenter/Session.qml | 2 +- modules/controlcenter/launcher/LauncherPane.qml | 446 ++++++++++++++++++++++++ 4 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 modules/controlcenter/launcher/LauncherPane.qml diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 1f8c6b3..ca36048 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -186,6 +186,11 @@ Item { icon: "task_alt" label: "taskbar" } + + NavItem { + icon: "apps" + label: "launcher" + } } component NavItem: Item { diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index b30bcf2..94bea9a 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -6,6 +6,7 @@ import "network" import "audio" import "appearance" import "taskbar" +import "launcher" import qs.components import qs.services import qs.config @@ -68,6 +69,13 @@ ClippingRectangle { } } + Pane { + index: 6 + sourceComponent: LauncherPane { + session: root.session + } + } + Behavior on y { Anim {} } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index cf31ec3..d44898e 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -2,7 +2,7 @@ import Quickshell.Bluetooth import QtQuick QtObject { - readonly property list panes: ["ethernet", "wireless", "bluetooth", "audio", "appearance", "taskbar"] + readonly property list panes: ["ethernet", "wireless", "bluetooth", "audio", "appearance", "taskbar", "launcher"] required property var root property bool floating: false diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml new file mode 100644 index 0000000..7377f42 --- /dev/null +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -0,0 +1,446 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../launcher/services" +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import qs.utils +import Caelestia +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property Session session + + property var selectedApp: null + property bool showDebugInfo: false + + anchors.fill: parent + + spacing: 0 + + FileView { + id: configFile + + path: `${Paths.config}/shell.json` + watchChanges: true + + onLoaded: { + try { + const config = JSON.parse(text()); + updateToggleState(); + } catch (e) { + console.error("Failed to parse config:", e); + } + } + } + + function updateToggleState() { + if (!root.selectedApp || !configFile.loaded) { + hideFromLauncherSwitch.checked = false; + return; + } + + try { + const config = JSON.parse(configFile.text()); + const appId = root.selectedApp.id || root.selectedApp.entry?.id; + + if (config.launcher && config.launcher.hiddenApps) { + hideFromLauncherSwitch.checked = config.launcher.hiddenApps.includes(appId); + } else { + hideFromLauncherSwitch.checked = false; + } + } catch (e) { + console.error("Failed to update toggle state:", e); + } + } + + function saveHiddenApps() { + if (!configFile.loaded || !root.selectedApp) { + return; + } + + try { + const config = JSON.parse(configFile.text()); + const appId = root.selectedApp.id || root.selectedApp.entry?.id; + + if (!config.launcher) config.launcher = {}; + if (!config.launcher.hiddenApps) config.launcher.hiddenApps = []; + + const hiddenApps = config.launcher.hiddenApps; + const isHidden = hideFromLauncherSwitch.checked; + + if (isHidden) { + // Add to hiddenApps if not already there + if (!hiddenApps.includes(appId)) { + hiddenApps.push(appId); + } + } else { + // Remove from hiddenApps + const index = hiddenApps.indexOf(appId); + if (index !== -1) { + hiddenApps.splice(index, 1); + } + } + + const jsonString = JSON.stringify(config, null, 4); + configFile.setText(jsonString); + } catch (e) { + console.error("Failed to save config:", e); + } + } + + onSelectedAppChanged: { + updateToggleState(); + } + + AppDb { + id: allAppsDb + + path: `${Paths.state}/apps.sqlite` + entries: DesktopEntries.applications.values // No filter - show all apps + } + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true + + ColumnLayout { + 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 + + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Applications (%1)").arg(allAppsDb.apps.length) + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("All applications available in the launcher") + color: Colours.palette.m3outline + } + + StyledListView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: Appearance.spacing.normal + + model: allAppsDb.apps + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + delegate: StyledRect { + required property var modelData + + anchors.left: parent.left + anchors.right: parent.right + + readonly property bool isSelected: root.selectedApp === modelData + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isSelected ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isSelected ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + root.selectedApp = modelData; + } + } + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + IconImage { + Layout.alignment: Qt.AlignVCenter + implicitSize: 32 + source: { + const entry = modelData.entry; + return entry ? Quickshell.iconPath(entry.icon, "image-missing") : "image-missing"; + } + } + + StyledText { + Layout.fillWidth: true + text: modelData.name || modelData.entry?.name || qsTr("Unknown") + font.pointSize: Appearance.font.size.normal + } + } + + implicitHeight: 40 + } + } + } + + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + spacing: Appearance.spacing.normal + + Item { + Layout.alignment: Qt.AlignHCenter + implicitWidth: iconLoader.implicitWidth + implicitHeight: iconLoader.implicitHeight + + Loader { + id: iconLoader + sourceComponent: root.selectedApp ? appIconComponent : defaultIconComponent + } + + Component { + id: appIconComponent + IconImage { + implicitSize: Appearance.font.size.extraLarge * 3 * 2 + source: { + if (!root.selectedApp) return "image-missing"; + const entry = root.selectedApp.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 { + Layout.alignment: Qt.AlignHCenter + text: root.selectedApp ? (root.selectedApp.name || root.selectedApp.entry?.name || qsTr("Application Details")) : qsTr("Launcher Applications") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: Appearance.spacing.large + + StyledFlickable { + anchors.fill: parent + flickableDirection: Flickable.VerticalFlick + contentHeight: debugLayout.implicitHeight + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + ColumnLayout { + id: debugLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal + + StyledRect { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + implicitHeight: hideToggleRow.implicitHeight + Appearance.padding.large * 2 + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal + + RowLayout { + id: hideToggleRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Hide from launcher") + font.pointSize: Appearance.font.size.normal + } + + StyledSwitch { + id: hideFromLauncherSwitch + checked: false + enabled: root.selectedApp !== null && configFile.loaded + onToggled: { + root.saveHiddenApps(); + } + } + } + } + + StyledRect { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + implicitHeight: debugToggleRow.implicitHeight + Appearance.padding.large * 2 + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal + + RowLayout { + id: debugToggleRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show debug information") + font.pointSize: Appearance.font.size.normal + } + + StyledSwitch { + id: showDebugInfoSwitch + checked: root.showDebugInfo + onToggled: { + root.showDebugInfo = checked; + } + } + } + } + + StyledRect { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + Layout.preferredHeight: 300 + visible: root.showDebugInfo + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal + clip: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Debug Info - All Available Properties") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + StyledFlickable { + Layout.fillWidth: true + Layout.fillHeight: true + flickableDirection: Flickable.VerticalFlick + contentHeight: debugText.implicitHeight + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + TextEdit { + id: debugText + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + width: parent.width + + text: { + if (!root.selectedApp) return "No app selected"; + + let debug = ""; + const app = root.selectedApp; + const entry = app.entry; + + debug += "=== App Properties ===\n"; + for (let prop in app) { + try { + const value = app[prop]; + debug += prop + ": " + (value !== null && value !== undefined ? String(value) : "null/undefined") + "\n"; + } catch (e) { + debug += prop + ": [error accessing]\n"; + } + } + + debug += "\n=== Entry Properties ===\n"; + if (entry) { + for (let prop in entry) { + try { + const value = entry[prop]; + debug += prop + ": " + (value !== null && value !== undefined ? String(value) : "null/undefined") + "\n"; + } catch (e) { + debug += prop + ": [error accessing]\n"; + } + } + } else { + debug += "entry is null\n"; + } + + return debug; + } + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + wrapMode: TextEdit.Wrap + readOnly: true + selectByMouse: true + selectByKeyboard: true + } + } + } + } + } + } + } + } + + InnerBorder { + leftThickness: Appearance.padding.normal / 2 + } + } +} -- cgit v1.2.3-freya From d2c5065aa9fe39e5a9bbbc96e1882dcb73acdab2 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 11:44:47 -0500 Subject: controlcenter: added debug toggle to taskbar objects --- modules/controlcenter/taskbar/TaskbarPane.qml | 37 ++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index a367635..7ada7b1 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -18,6 +18,8 @@ RowLayout { required property Session session + property bool showDebugInfo: false + anchors.fill: parent spacing: 0 @@ -98,7 +100,6 @@ RowLayout { } debugInfo += `\nFinal entries array:\n${JSON.stringify(config.bar.entries, null, 2)}\n`; - debugInfo += `\nFinal clock.showIcon: ${config.bar.clock.showIcon}\n`; root.debugInfo = debugInfo; // Write back to file using setText (same simple approach that worked for clock) @@ -338,9 +339,41 @@ RowLayout { color: Colours.palette.m3outline } + StyledRect { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.large + implicitHeight: debugToggleRow.implicitHeight + Appearance.padding.large * 2 + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal + + RowLayout { + id: debugToggleRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show debug information") + font.pointSize: Appearance.font.size.normal + } + + StyledSwitch { + id: showDebugInfoSwitch + checked: root.showDebugInfo + onToggled: { + root.showDebugInfo = checked; + } + } + } + } + StyledText { Layout.topMargin: Appearance.spacing.large Layout.alignment: Qt.AlignHCenter + visible: root.showDebugInfo text: qsTr("Debug Info") font.pointSize: Appearance.font.size.larger font.weight: 500 @@ -350,6 +383,7 @@ RowLayout { Layout.fillWidth: true Layout.preferredHeight: 200 Layout.maximumHeight: 300 + visible: root.showDebugInfo radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer @@ -387,6 +421,7 @@ RowLayout { StyledText { Layout.topMargin: Appearance.spacing.small Layout.alignment: Qt.AlignHCenter + visible: root.showDebugInfo text: root.lastSaveStatus || "" font.pointSize: Appearance.font.size.small color: root.lastSaveStatus.includes("Error") ? Colours.palette.m3error : Colours.palette.m3primary -- cgit v1.2.3-freya From 5a0c2171d39f118025ec52432cb538ebda72f9a8 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 12:25:51 -0500 Subject: controlcenter: taskbar panel now configures all aspects of shell.json bar object except for scroll actions --- modules/controlcenter/taskbar/TaskbarPane.qml | 1273 +++++++++++++++++++++---- 1 file changed, 1097 insertions(+), 176 deletions(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 7ada7b1..105bbe1 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -20,252 +20,1174 @@ RowLayout { property bool showDebugInfo: false + // Bar Behavior + property bool persistent: true + property bool showOnHover: true + property int dragThreshold: 20 + + // Status Icons + property bool showAudio: true + property bool showMicrophone: true + property bool showKbLayout: false + property bool showNetwork: true + property bool showBluetooth: true + property bool showBattery: true + property bool showLockStatus: true + + // Tray Settings + property bool trayBackground: false + property bool trayCompact: false + property bool trayRecolour: false + + // Workspaces + property int workspacesShown: 5 + property bool workspacesActiveIndicator: true + property bool workspacesOccupiedBg: false + property bool workspacesShowWindows: false + property bool workspacesPerMonitor: true + anchors.fill: parent - spacing: 0 + spacing: 0 + + FileView { + id: configFile + + path: `${Paths.config}/shell.json` + watchChanges: true + + onLoaded: { + try { + const config = JSON.parse(text()); + updateFromConfig(config); + } catch (e) { + console.error("Failed to parse config:", e); + } + } + } + + function updateFromConfig(config) { + // Update clock toggle + if (config.bar && config.bar.clock) { + clockShowIconSwitch.checked = config.bar.clock.showIcon !== false; + } + + // Update entries + if (config.bar && config.bar.entries) { + entriesModel.clear(); + for (const entry of config.bar.entries) { + entriesModel.append({ + id: entry.id, + enabled: entry.enabled !== false + }); + } + } + + // Update bar behavior + if (config.bar) { + root.persistent = config.bar.persistent !== false; + root.showOnHover = config.bar.showOnHover !== false; + root.dragThreshold = config.bar.dragThreshold || 20; + } + + // Update status icons + if (config.bar && config.bar.status) { + root.showAudio = config.bar.status.showAudio !== false; + root.showMicrophone = config.bar.status.showMicrophone !== false; + root.showKbLayout = config.bar.status.showKbLayout === true; + root.showNetwork = config.bar.status.showNetwork !== false; + root.showBluetooth = config.bar.status.showBluetooth !== false; + root.showBattery = config.bar.status.showBattery !== false; + root.showLockStatus = config.bar.status.showLockStatus !== false; + } + + // Update tray settings + if (config.bar && config.bar.tray) { + root.trayBackground = config.bar.tray.background === true; + root.trayCompact = config.bar.tray.compact === true; + root.trayRecolour = config.bar.tray.recolour === true; + } + + // Update workspaces + if (config.bar && config.bar.workspaces) { + root.workspacesShown = config.bar.workspaces.shown || 5; + root.workspacesActiveIndicator = config.bar.workspaces.activeIndicator !== false; + root.workspacesOccupiedBg = config.bar.workspaces.occupiedBg === true; + root.workspacesShowWindows = config.bar.workspaces.showWindows === true; + root.workspacesPerMonitor = config.bar.workspaces.perMonitorWorkspaces !== false; + } + } + + function saveConfig(entryIndex, entryEnabled) { + if (!configFile.loaded) { + root.lastSaveStatus = "Error: Config file not loaded yet"; + root.debugInfo = "Config file not loaded yet, cannot save"; + return; + } + + try { + const config = JSON.parse(configFile.text()); + + // Ensure bar object exists + if (!config.bar) config.bar = {}; + + // Update clock setting + if (!config.bar.clock) config.bar.clock = {}; + config.bar.clock.showIcon = clockShowIconSwitch.checked; + + // Update bar behavior + config.bar.persistent = root.persistent; + config.bar.showOnHover = root.showOnHover; + config.bar.dragThreshold = root.dragThreshold; + + // Update status icons + if (!config.bar.status) config.bar.status = {}; + config.bar.status.showAudio = root.showAudio; + config.bar.status.showMicrophone = root.showMicrophone; + config.bar.status.showKbLayout = root.showKbLayout; + config.bar.status.showNetwork = root.showNetwork; + config.bar.status.showBluetooth = root.showBluetooth; + config.bar.status.showBattery = root.showBattery; + config.bar.status.showLockStatus = root.showLockStatus; + + // Update tray settings + if (!config.bar.tray) config.bar.tray = {}; + config.bar.tray.background = root.trayBackground; + config.bar.tray.compact = root.trayCompact; + config.bar.tray.recolour = root.trayRecolour; + + // Update workspaces + if (!config.bar.workspaces) config.bar.workspaces = {}; + config.bar.workspaces.shown = root.workspacesShown; + config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; + config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; + config.bar.workspaces.showWindows = root.workspacesShowWindows; + config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; + + // Update entries from the model (same approach as clock - use provided value if available) + if (!config.bar.entries) config.bar.entries = []; + config.bar.entries = []; + + let debugInfo = `saveConfig called\n`; + debugInfo += `entryIndex: ${entryIndex}\n`; + debugInfo += `entryEnabled: ${entryEnabled}\n`; + debugInfo += `entriesModel.count: ${entriesModel.count}\n\n`; + + for (let i = 0; i < entriesModel.count; i++) { + const entry = entriesModel.get(i); + // If this is the entry being updated, use the provided value (same as clock toggle reads from switch) + // Otherwise use the value from the model + let enabled = entry.enabled; + if (entryIndex !== undefined && i === entryIndex) { + enabled = entryEnabled; + debugInfo += `Entry ${i} (${entry.id}): Using provided value = ${entryEnabled}\n`; + } else { + debugInfo += `Entry ${i} (${entry.id}): Using model value = ${entry.enabled}\n`; + } + config.bar.entries.push({ + id: entry.id, + enabled: enabled + }); + } + + debugInfo += `\nFinal entries array:\n${JSON.stringify(config.bar.entries, null, 2)}\n`; + root.debugInfo = debugInfo; + + // Write back to file using setText (same simple approach that worked for clock) + const jsonString = JSON.stringify(config, null, 4); + configFile.setText(jsonString); + root.lastSaveStatus = `Saved! Entries count: ${config.bar.entries.length}`; + } catch (e) { + root.lastSaveStatus = `Error: ${e.message}`; + root.debugInfo = `Failed to save config:\n${e.message}\n${e.stack}`; + } + } + + ListModel { + id: entriesModel + } + + // Debug info + property string debugInfo: "" + property string lastSaveStatus: "" + + function collapseAllSections(exceptSection) { + if (exceptSection !== clockSection) clockSection.expanded = false; + if (exceptSection !== barBehaviorSection) barBehaviorSection.expanded = false; + if (exceptSection !== statusIconsSection) statusIconsSection.expanded = false; + if (exceptSection !== traySettingsSection) traySettingsSection.expanded = false; + if (exceptSection !== workspacesSection) workspacesSection.expanded = false; + } + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true + + StyledFlickable { + id: sidebarFlickable + anchors.fill: parent + flickableDirection: Flickable.VerticalFlick + contentHeight: sidebarLayout.implicitHeight + Appearance.padding.large * 2 + + StyledScrollBar.vertical: StyledScrollBar { + flickable: sidebarFlickable + } + + ColumnLayout { + id: sidebarLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 + + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + } + + Item { + id: clockSection + Layout.fillWidth: true + Layout.preferredHeight: clockSectionHeader.implicitHeight + property bool expanded: false + + ColumnLayout { + id: clockSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Clock") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: clockSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = clockSection.expanded; + root.collapseAllSections(clockSection); + clockSection.expanded = !wasExpanded; + } + } + + StyledText { + visible: clockSection.expanded + text: qsTr("Clock display settings") + color: Colours.palette.m3outline + Layout.fillWidth: true + } + } + } + + StyledRect { + Layout.fillWidth: true + visible: clockSection.expanded + implicitHeight: clockSection.expanded ? clockRow.implicitHeight + Appearance.padding.large * 2 : 0 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: clockRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show clock icon") + } + + StyledSwitch { + id: clockShowIconSwitch + checked: true + onToggled: { + root.saveConfig(); + } + } + } + } + + Item { + id: barBehaviorSection + Layout.fillWidth: true + Layout.preferredHeight: barBehaviorSectionHeader.implicitHeight + property bool expanded: false + + ColumnLayout { + id: barBehaviorSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Bar Behavior") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: barBehaviorSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = barBehaviorSection.expanded; + root.collapseAllSections(barBehaviorSection); + barBehaviorSection.expanded = !wasExpanded; + } + } + } + } + + StyledRect { + visible: barBehaviorSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: barBehaviorSection.expanded ? persistentRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: persistentRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Persistent") + } + + StyledSwitch { + checked: root.persistent + onToggled: { + root.persistent = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: barBehaviorSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: barBehaviorSection.expanded ? showOnHoverRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: showOnHoverRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show on hover") + } + + StyledSwitch { + checked: root.showOnHover + onToggled: { + root.showOnHover = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: barBehaviorSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: barBehaviorSection.expanded ? dragThresholdRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: dragThresholdRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Drag threshold") + } + + CustomSpinBox { + min: 0 + max: 100 + value: root.dragThreshold + onValueModified: value => { + root.dragThreshold = value; + root.saveConfig(); + } + } + } + } + + Item { + id: statusIconsSection + Layout.fillWidth: true + Layout.preferredHeight: statusIconsSectionHeader.implicitHeight + property bool expanded: false + + ColumnLayout { + id: statusIconsSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Status Icons") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: statusIconsSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = statusIconsSection.expanded; + root.collapseAllSections(statusIconsSection); + statusIconsSection.expanded = !wasExpanded; + } + } + } + } + + StyledRect { + visible: statusIconsSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: statusIconsSection.expanded ? showAudioRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: showAudioRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show audio") + } + + StyledSwitch { + checked: root.showAudio + onToggled: { + root.showAudio = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: statusIconsSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: statusIconsSection.expanded ? showMicrophoneRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: showMicrophoneRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show microphone") + } + + StyledSwitch { + checked: root.showMicrophone + onToggled: { + root.showMicrophone = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: statusIconsSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: statusIconsSection.expanded ? showKbLayoutRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: showKbLayoutRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show keyboard layout") + } + + StyledSwitch { + checked: root.showKbLayout + onToggled: { + root.showKbLayout = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: statusIconsSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: statusIconsSection.expanded ? showNetworkRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: showNetworkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show network") + } + + StyledSwitch { + checked: root.showNetwork + onToggled: { + root.showNetwork = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: statusIconsSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: statusIconsSection.expanded ? showBluetoothRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: showBluetoothRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show bluetooth") + } + + StyledSwitch { + checked: root.showBluetooth + onToggled: { + root.showBluetooth = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: statusIconsSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: statusIconsSection.expanded ? showBatteryRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: showBatteryRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show battery") + } + + StyledSwitch { + checked: root.showBattery + onToggled: { + root.showBattery = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: statusIconsSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: statusIconsSection.expanded ? showLockStatusRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: showLockStatusRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show lock status") + } + + StyledSwitch { + checked: root.showLockStatus + onToggled: { + root.showLockStatus = checked; + root.saveConfig(); + } + } + } + } + + Item { + id: traySettingsSection + Layout.fillWidth: true + Layout.preferredHeight: traySettingsSectionHeader.implicitHeight + property bool expanded: false + + ColumnLayout { + id: traySettingsSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Tray Settings") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: traySettingsSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = traySettingsSection.expanded; + root.collapseAllSections(traySettingsSection); + traySettingsSection.expanded = !wasExpanded; + } + } + } + } + + StyledRect { + visible: traySettingsSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: traySettingsSection.expanded ? trayBackgroundRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: trayBackgroundRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Background") + } + + StyledSwitch { + checked: root.trayBackground + onToggled: { + root.trayBackground = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: traySettingsSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: traySettingsSection.expanded ? trayCompactRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } - FileView { - id: configFile + RowLayout { + id: trayCompactRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - path: `${Paths.config}/shell.json` - watchChanges: true + StyledText { + Layout.fillWidth: true + text: qsTr("Compact") + } - onLoaded: { - try { - const config = JSON.parse(text()); - updateFromConfig(config); - } catch (e) { - console.error("Failed to parse config:", e); + StyledSwitch { + checked: root.trayCompact + onToggled: { + root.trayCompact = checked; + root.saveConfig(); + } + } + } } - } - } - function updateFromConfig(config) { - // Update clock toggle - if (config.bar && config.bar.clock) { - clockShowIconSwitch.checked = config.bar.clock.showIcon !== false; - } + StyledRect { + visible: traySettingsSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: traySettingsSection.expanded ? trayRecolourRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - // Update entries - if (config.bar && config.bar.entries) { - entriesModel.clear(); - for (const entry of config.bar.entries) { - entriesModel.append({ - id: entry.id, - enabled: entry.enabled !== false - }); - } - } - } + Behavior on implicitHeight { + Anim {} + } - function saveConfig(entryIndex, entryEnabled) { - if (!configFile.loaded) { - root.lastSaveStatus = "Error: Config file not loaded yet"; - root.debugInfo = "Config file not loaded yet, cannot save"; - return; - } - - try { - const config = JSON.parse(configFile.text()); - - // Update clock setting (same simple approach - read directly from the switch) - if (!config.bar) config.bar = {}; - if (!config.bar.clock) config.bar.clock = {}; - config.bar.clock.showIcon = clockShowIconSwitch.checked; + RowLayout { + id: trayRecolourRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - // Update entries from the model (same approach as clock - use provided value if available) - if (!config.bar.entries) config.bar.entries = []; - config.bar.entries = []; - - let debugInfo = `saveConfig called\n`; - debugInfo += `entryIndex: ${entryIndex}\n`; - debugInfo += `entryEnabled: ${entryEnabled}\n`; - debugInfo += `entriesModel.count: ${entriesModel.count}\n\n`; - - for (let i = 0; i < entriesModel.count; i++) { - const entry = entriesModel.get(i); - // If this is the entry being updated, use the provided value (same as clock toggle reads from switch) - // Otherwise use the value from the model - let enabled = entry.enabled; - if (entryIndex !== undefined && i === entryIndex) { - enabled = entryEnabled; - debugInfo += `Entry ${i} (${entry.id}): Using provided value = ${entryEnabled}\n`; - } else { - debugInfo += `Entry ${i} (${entry.id}): Using model value = ${entry.enabled}\n`; + StyledText { + Layout.fillWidth: true + text: qsTr("Recolour") + } + + StyledSwitch { + checked: root.trayRecolour + onToggled: { + root.trayRecolour = checked; + root.saveConfig(); + } + } } - config.bar.entries.push({ - id: entry.id, - enabled: enabled - }); } - debugInfo += `\nFinal entries array:\n${JSON.stringify(config.bar.entries, null, 2)}\n`; - root.debugInfo = debugInfo; + Item { + id: workspacesSection + Layout.fillWidth: true + Layout.preferredHeight: workspacesSectionHeader.implicitHeight + property bool expanded: false - // Write back to file using setText (same simple approach that worked for clock) - const jsonString = JSON.stringify(config, null, 4); - configFile.setText(jsonString); - root.lastSaveStatus = `Saved! Entries count: ${config.bar.entries.length}`; - } catch (e) { - root.lastSaveStatus = `Error: ${e.message}`; - root.debugInfo = `Failed to save config:\n${e.message}\n${e.stack}`; - } - } + ColumnLayout { + id: workspacesSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small - ListModel { - id: entriesModel - } + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - // Debug info - property string debugInfo: "" - property string lastSaveStatus: "" + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Workspaces") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } - Item { - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true + Item { + Layout.fillWidth: true + } - ColumnLayout { - 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 + MaterialIcon { + text: "expand_more" + rotation: workspacesSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } - spacing: Appearance.spacing.small + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = workspacesSection.expanded; + root.collapseAllSections(workspacesSection); + workspacesSection.expanded = !wasExpanded; + } + } + } + } - RowLayout { - spacing: Appearance.spacing.smaller + StyledRect { + visible: workspacesSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: workspacesSection.expanded ? workspacesShownRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 + Behavior on implicitHeight { + Anim {} } - Item { - Layout.fillWidth: true - } - } + RowLayout { + id: workspacesShownRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Clock") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + StyledText { + Layout.fillWidth: true + text: qsTr("Shown") + } - StyledText { - text: qsTr("Clock display settings") - color: Colours.palette.m3outline + CustomSpinBox { + min: 1 + max: 20 + value: root.workspacesShown + onValueModified: value => { + root.workspacesShown = value; + root.saveConfig(); + } + } + } } StyledRect { + visible: workspacesSection.expanded Layout.fillWidth: true - implicitHeight: clockRow.implicitHeight + Appearance.padding.large * 2 - + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: workspacesSection.expanded ? workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 : 0 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer - RowLayout { - id: clockRow + Behavior on implicitHeight { + Anim {} + } + RowLayout { + id: workspacesActiveIndicatorRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true - text: qsTr("Show clock icon") + text: qsTr("Active indicator") } StyledSwitch { - id: clockShowIconSwitch - checked: true + checked: root.workspacesActiveIndicator onToggled: { + root.workspacesActiveIndicator = checked; root.saveConfig(); } } } } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Taskbar Entries") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + StyledRect { + visible: workspacesSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: workspacesSection.expanded ? workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - StyledText { - text: qsTr("Enable or disable taskbar entries") - color: Colours.palette.m3outline - } + Behavior on implicitHeight { + Anim {} + } - StyledListView { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.topMargin: 0 + RowLayout { + id: workspacesOccupiedBgRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - model: entriesModel - spacing: Appearance.spacing.small / 2 - clip: true + StyledText { + Layout.fillWidth: true + text: qsTr("Occupied background") + } - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent + StyledSwitch { + checked: root.workspacesOccupiedBg + onToggled: { + root.workspacesOccupiedBg = checked; + root.saveConfig(); + } + } } + } + + StyledRect { + visible: workspacesSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: workspacesSection.expanded ? workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - delegate: StyledRect { - id: delegate - required property string id - required property bool enabled - required property int index + Behavior on implicitHeight { + Anim {} + } + RowLayout { + id: workspacesShowWindowsRow anchors.left: parent.left anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal + StyledText { + Layout.fillWidth: true + text: qsTr("Show windows") + } - RowLayout { - id: entryRow + StyledSwitch { + checked: root.workspacesShowWindows + onToggled: { + root.workspacesShowWindows = checked; + root.saveConfig(); + } + } + } + } - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + StyledRect { + visible: workspacesSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: workspacesSection.expanded ? workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - spacing: Appearance.spacing.normal + Behavior on implicitHeight { + Anim {} + } - StyledText { - Layout.fillWidth: true - text: id.charAt(0).toUpperCase() + id.slice(1) - font.weight: enabled ? 500 : 400 - } + RowLayout { + id: workspacesPerMonitorRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - StyledSwitch { - checked: enabled - onToggled: { - // Store the values in local variables to ensure they're accessible - // Access index from the delegate - const entryIndex = delegate.index; - const entryEnabled = checked; - console.log(`Entry toggle: index=${entryIndex}, checked=${entryEnabled}`); - // Update the model first - entriesModel.setProperty(entryIndex, "enabled", entryEnabled); - // Save immediately with the value directly (same technique as clock toggle) - // Clock toggle reads directly from clockShowIconSwitch.checked - // We pass the value directly here (same approach) - root.saveConfig(entryIndex, entryEnabled); - } - } + StyledText { + Layout.fillWidth: true + text: qsTr("Per monitor workspaces") } - implicitHeight: entryRow.implicitHeight + Appearance.padding.normal * 2 + StyledSwitch { + checked: root.workspacesPerMonitor + onToggled: { + root.workspacesPerMonitor = checked; + root.saveConfig(); + } + } } } } + } InnerBorder { leftThickness: 0 @@ -434,4 +1356,3 @@ RowLayout { } } } - -- cgit v1.2.3-freya From 058a289a04593a783110eec3d3801afea46e9525 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 12:42:28 -0500 Subject: controlcenter: appearance panel, more shell.json settings available --- .../controlcenter/appearance/AppearancePane.qml | 617 +++++++++++++++++++-- 1 file changed, 582 insertions(+), 35 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index c8a4522..b8e02e1 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -11,6 +11,7 @@ import qs.services import qs.config import Caelestia.Models import Quickshell +import Quickshell.Io import QtQuick import QtQuick.Layouts @@ -19,22 +20,145 @@ RowLayout { required property Session session + // Background settings + property bool desktopClockEnabled: true + property bool backgroundEnabled: true + property bool visualiserEnabled: true + property bool visualiserAutoHide: true + property real visualiserRounding: 1 + property real visualiserSpacing: 1 + anchors.fill: parent spacing: 0 + FileView { + id: configFile + + path: `${Paths.config}/shell.json` + watchChanges: true + + property bool isSaving: false + + onLoaded: { + if (!isSaving) { + try { + const config = JSON.parse(text()); + updateFromConfig(config); + } catch (e) { + console.error("Failed to parse config:", e); + } + } + } + + onFileChanged: { + if (!isSaving) { + try { + const config = JSON.parse(text()); + updateFromConfig(config); + } catch (e) { + console.error("Failed to parse config:", e); + } + } + } + } + + function updateFromConfig(config) { + // Update background settings + if (config.background) { + root.desktopClockEnabled = config.background.desktopClock?.enabled !== false; + root.backgroundEnabled = config.background.enabled !== false; + if (config.background.visualiser) { + root.visualiserEnabled = config.background.visualiser.enabled !== false; + root.visualiserAutoHide = config.background.visualiser.autoHide !== false; + root.visualiserRounding = config.background.visualiser.rounding || 1; + root.visualiserSpacing = config.background.visualiser.spacing || 1; + } + } + } + + Timer { + id: saveTimer + interval: 500 + onTriggered: { + configFile.isSaving = false; + } + } + + function collapseAllSections(exceptSection) { + if (exceptSection !== themeModeSection) themeModeSection.expanded = false; + if (exceptSection !== colorVariantSection) colorVariantSection.expanded = false; + if (exceptSection !== colorSchemeSection) colorSchemeSection.expanded = false; + if (exceptSection !== backgroundSection) backgroundSection.expanded = false; + } + + function saveConfig() { + if (!configFile.loaded) { + console.error("Config file not loaded yet"); + return; + } + + try { + // Set flag to prevent reloading during save + configFile.isSaving = true; + + const config = JSON.parse(configFile.text()); + + // Ensure background object exists + if (!config.background) config.background = {}; + + // Update desktop clock + if (!config.background.desktopClock) config.background.desktopClock = {}; + config.background.desktopClock.enabled = root.desktopClockEnabled; + + // Update background enabled + config.background.enabled = root.backgroundEnabled; + + // Update visualiser + if (!config.background.visualiser) config.background.visualiser = {}; + config.background.visualiser.enabled = root.visualiserEnabled; + config.background.visualiser.autoHide = root.visualiserAutoHide; + config.background.visualiser.rounding = root.visualiserRounding; + config.background.visualiser.spacing = root.visualiserSpacing; + + // Write back to file + const jsonString = JSON.stringify(config, null, 4); + configFile.setText(jsonString); + + // Reset flag after a delay to allow file write to complete + // Use a timer to ensure the file system has time to write + saveTimer.restart(); + } catch (e) { + console.error("Failed to save config:", e); + configFile.isSaving = false; + } + } + Item { Layout.preferredWidth: Math.floor(parent.width * 0.4) Layout.minimumWidth: 420 Layout.fillHeight: true - ColumnLayout { + StyledFlickable { + id: sidebarFlickable 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 + flickableDirection: Flickable.VerticalFlick + contentHeight: sidebarLayout.implicitHeight + Appearance.padding.large * 2 - spacing: Appearance.spacing.small + StyledScrollBar.vertical: StyledScrollBar { + flickable: sidebarFlickable + } + + ColumnLayout { + id: sidebarLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 + + spacing: Appearance.spacing.small RowLayout { spacing: Appearance.spacing.smaller @@ -50,25 +174,75 @@ RowLayout { } } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Theme mode") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + Item { + id: themeModeSection + Layout.fillWidth: true + Layout.preferredHeight: themeModeSectionHeader.implicitHeight + property bool expanded: false - StyledText { - text: qsTr("Light or dark theme") - color: Colours.palette.m3outline + ColumnLayout { + id: themeModeSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Theme mode") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: themeModeSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = themeModeSection.expanded; + root.collapseAllSections(themeModeSection); + themeModeSection.expanded = !wasExpanded; + } + } + + StyledText { + visible: themeModeSection.expanded + text: qsTr("Light or dark theme") + color: Colours.palette.m3outline + Layout.fillWidth: true + } + } } StyledRect { + visible: themeModeSection.expanded Layout.fillWidth: true - implicitHeight: modeToggle.implicitHeight + Appearance.padding.large * 2 + implicitHeight: themeModeSection.expanded ? modeToggle.implicitHeight + Appearance.padding.large * 2 : 0 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer + Behavior on implicitHeight { + Anim {} + } + RowLayout { id: modeToggle @@ -93,23 +267,73 @@ RowLayout { } } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Color variant") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + Item { + id: colorVariantSection + Layout.fillWidth: true + Layout.preferredHeight: colorVariantSectionHeader.implicitHeight + property bool expanded: false - StyledText { - text: qsTr("Material theme variant") - color: Colours.palette.m3outline + ColumnLayout { + id: colorVariantSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Color variant") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: colorVariantSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = colorVariantSection.expanded; + root.collapseAllSections(colorVariantSection); + colorVariantSection.expanded = !wasExpanded; + } + } + + StyledText { + visible: colorVariantSection.expanded + text: qsTr("Material theme variant") + color: Colours.palette.m3outline + Layout.fillWidth: true + } + } } StyledListView { + visible: colorVariantSection.expanded Layout.fillWidth: true - Layout.fillHeight: true + implicitHeight: colorVariantSection.expanded ? Math.min(400, M3Variants.list.length * 60) : 0 Layout.topMargin: 0 + Behavior on implicitHeight { + Anim {} + } + model: M3Variants.list spacing: Appearance.spacing.small / 2 clip: true @@ -188,23 +412,73 @@ RowLayout { } } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Color scheme") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + Item { + id: colorSchemeSection + Layout.fillWidth: true + Layout.preferredHeight: colorSchemeSectionHeader.implicitHeight + property bool expanded: false - StyledText { - text: qsTr("Available color schemes") - color: Colours.palette.m3outline + ColumnLayout { + id: colorSchemeSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Color scheme") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: colorSchemeSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = colorSchemeSection.expanded; + root.collapseAllSections(colorSchemeSection); + colorSchemeSection.expanded = !wasExpanded; + } + } + + StyledText { + visible: colorSchemeSection.expanded + text: qsTr("Available color schemes") + color: Colours.palette.m3outline + Layout.fillWidth: true + } + } } StyledListView { + visible: colorSchemeSection.expanded Layout.fillWidth: true - Layout.fillHeight: true + implicitHeight: colorSchemeSection.expanded ? Math.min(400, Schemes.list.length * 80) : 0 Layout.topMargin: 0 + Behavior on implicitHeight { + Anim {} + } + model: Schemes.list spacing: Appearance.spacing.small / 2 clip: true @@ -339,6 +613,279 @@ RowLayout { implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 } } + + Item { + id: backgroundSection + Layout.fillWidth: true + Layout.preferredHeight: backgroundSectionHeader.implicitHeight + property bool expanded: false + + ColumnLayout { + id: backgroundSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Background") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: backgroundSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = backgroundSection.expanded; + root.collapseAllSections(backgroundSection); + backgroundSection.expanded = !wasExpanded; + } + } + } + } + + StyledRect { + visible: backgroundSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: backgroundSection.expanded ? desktopClockRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: desktopClockRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Desktop clock") + } + + StyledSwitch { + checked: root.desktopClockEnabled + onToggled: { + root.desktopClockEnabled = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: backgroundSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: backgroundSection.expanded ? backgroundEnabledRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: backgroundEnabledRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Background enabled") + } + + StyledSwitch { + checked: root.backgroundEnabled + onToggled: { + root.backgroundEnabled = checked; + root.saveConfig(); + } + } + } + } + + StyledText { + visible: backgroundSection.expanded + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Visualiser") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledRect { + visible: backgroundSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: backgroundSection.expanded ? visualiserEnabledRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: visualiserEnabledRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Visualiser enabled") + } + + StyledSwitch { + checked: root.visualiserEnabled + onToggled: { + root.visualiserEnabled = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: backgroundSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: backgroundSection.expanded ? visualiserAutoHideRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: visualiserAutoHideRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Visualiser auto hide") + } + + StyledSwitch { + checked: root.visualiserAutoHide + onToggled: { + root.visualiserAutoHide = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: backgroundSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: backgroundSection.expanded ? visualiserRoundingRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: visualiserRoundingRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Visualiser rounding") + } + + CustomSpinBox { + min: 0 + max: 10 + value: Math.round(root.visualiserRounding) + onValueModified: value => { + root.visualiserRounding = value; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: backgroundSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: backgroundSection.expanded ? visualiserSpacingRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: visualiserSpacingRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Visualiser spacing") + } + + CustomSpinBox { + min: 0 + max: 10 + value: Math.round(root.visualiserSpacing) + onValueModified: value => { + root.visualiserSpacing = value; + root.saveConfig(); + } + } + } + } + } } InnerBorder { -- cgit v1.2.3-freya From c9d13d518eb051b175572e1d0bcf988d0b2555d1 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 15:11:47 -0500 Subject: controlcenter: apperance panel, mostly finished --- .../controlcenter/appearance/AppearancePane.qml | 76 ++++++++++------------ 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index b8e02e1..ffde0c0 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -9,6 +9,7 @@ import qs.components.containers import qs.components.images import qs.services import qs.config +import qs.utils import Caelestia.Models import Quickshell import Quickshell.Io @@ -38,50 +39,45 @@ RowLayout { path: `${Paths.config}/shell.json` watchChanges: true - property bool isSaving: false - onLoaded: { - if (!isSaving) { - try { - const config = JSON.parse(text()); - updateFromConfig(config); - } catch (e) { - console.error("Failed to parse config:", e); - } + try { + const config = JSON.parse(text()); + updateFromConfig(config); + } catch (e) { + console.error("Failed to parse config:", e); } } - onFileChanged: { - if (!isSaving) { - try { - const config = JSON.parse(text()); - updateFromConfig(config); - } catch (e) { - console.error("Failed to parse config:", e); - } - } + onSaveFailed: err => { + console.error("Failed to save config file:", err); } } function updateFromConfig(config) { // Update background settings if (config.background) { - root.desktopClockEnabled = config.background.desktopClock?.enabled !== false; - root.backgroundEnabled = config.background.enabled !== false; + root.desktopClockEnabled = config.background.desktopClock?.enabled !== undefined ? config.background.desktopClock.enabled : false; + root.backgroundEnabled = config.background.enabled !== undefined ? config.background.enabled : true; if (config.background.visualiser) { - root.visualiserEnabled = config.background.visualiser.enabled !== false; - root.visualiserAutoHide = config.background.visualiser.autoHide !== false; - root.visualiserRounding = config.background.visualiser.rounding || 1; - root.visualiserSpacing = config.background.visualiser.spacing || 1; + root.visualiserEnabled = config.background.visualiser.enabled !== undefined ? config.background.visualiser.enabled : false; + root.visualiserAutoHide = config.background.visualiser.autoHide !== undefined ? config.background.visualiser.autoHide : true; + root.visualiserRounding = config.background.visualiser.rounding !== undefined ? config.background.visualiser.rounding : 1; + root.visualiserSpacing = config.background.visualiser.spacing !== undefined ? config.background.visualiser.spacing : 1; + } else { + // Set defaults if visualiser object doesn't exist (matching BackgroundConfig defaults) + root.visualiserEnabled = false; + root.visualiserAutoHide = true; + root.visualiserRounding = 1; + root.visualiserSpacing = 1; } - } - } - - Timer { - id: saveTimer - interval: 500 - onTriggered: { - configFile.isSaving = false; + } else { + // Set defaults if background object doesn't exist (matching BackgroundConfig defaults) + root.desktopClockEnabled = false; + root.backgroundEnabled = true; + root.visualiserEnabled = false; + root.visualiserAutoHide = true; + root.visualiserRounding = 1; + root.visualiserSpacing = 1; } } @@ -99,9 +95,6 @@ RowLayout { } try { - // Set flag to prevent reloading during save - configFile.isSaving = true; - const config = JSON.parse(configFile.text()); // Ensure background object exists @@ -121,16 +114,11 @@ RowLayout { config.background.visualiser.rounding = root.visualiserRounding; config.background.visualiser.spacing = root.visualiserSpacing; - // Write back to file + // Write back to file using setText (same simple approach that worked for taskbar) const jsonString = JSON.stringify(config, null, 4); configFile.setText(jsonString); - - // Reset flag after a delay to allow file write to complete - // Use a timer to ensure the file system has time to write - saveTimer.restart(); } catch (e) { console.error("Failed to save config:", e); - configFile.isSaving = false; } } @@ -690,6 +678,7 @@ RowLayout { } StyledSwitch { + id: desktopClockSwitch checked: root.desktopClockEnabled onToggled: { root.desktopClockEnabled = checked; @@ -725,6 +714,7 @@ RowLayout { } StyledSwitch { + id: backgroundEnabledSwitch checked: root.backgroundEnabled onToggled: { root.backgroundEnabled = checked; @@ -768,6 +758,7 @@ RowLayout { } StyledSwitch { + id: visualiserEnabledSwitch checked: root.visualiserEnabled onToggled: { root.visualiserEnabled = checked; @@ -803,6 +794,7 @@ RowLayout { } StyledSwitch { + id: visualiserAutoHideSwitch checked: root.visualiserAutoHide onToggled: { root.visualiserAutoHide = checked; @@ -838,6 +830,7 @@ RowLayout { } CustomSpinBox { + id: visualiserRoundingSpinBox min: 0 max: 10 value: Math.round(root.visualiserRounding) @@ -875,6 +868,7 @@ RowLayout { } CustomSpinBox { + id: visualiserSpacingSpinBox min: 0 max: 10 value: Math.round(root.visualiserSpacing) -- cgit v1.2.3-freya From 4188d328401b47e07b4918ff250fff6d22128f55 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 15:50:41 -0500 Subject: controlcenter: appearance panel, now does all shell.json options --- .../controlcenter/appearance/AppearancePane.qml | 821 +++++++++++++++++++++ 1 file changed, 821 insertions(+) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index ffde0c0..4bf6d44 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -21,6 +21,19 @@ RowLayout { required property Session session + // Appearance settings + property real animDurationsScale: 1 + property string fontFamilyMaterial: "Material Symbols Rounded" + property string fontFamilyMono: "CaskaydiaCove NF" + property string fontFamilySans: "Rubik" + property real fontSizeScale: 1 + property real paddingScale: 1 + property real roundingScale: 1 + property real spacingScale: 1 + property bool transparencyEnabled: false + property real transparencyBase: 0.85 + property real transparencyLayers: 0.4 + // Background settings property bool desktopClockEnabled: true property bool backgroundEnabled: true @@ -54,6 +67,37 @@ RowLayout { } function updateFromConfig(config) { + // Update appearance settings + if (config.appearance) { + if (config.appearance.anim && config.appearance.anim.durations) { + root.animDurationsScale = config.appearance.anim.durations.scale ?? 1; + } + if (config.appearance.font) { + if (config.appearance.font.family) { + root.fontFamilyMaterial = config.appearance.font.family.material ?? "Material Symbols Rounded"; + root.fontFamilyMono = config.appearance.font.family.mono ?? "CaskaydiaCove NF"; + root.fontFamilySans = config.appearance.font.family.sans ?? "Rubik"; + } + if (config.appearance.font.size) { + root.fontSizeScale = config.appearance.font.size.scale ?? 1; + } + } + if (config.appearance.padding) { + root.paddingScale = config.appearance.padding.scale ?? 1; + } + if (config.appearance.rounding) { + root.roundingScale = config.appearance.rounding.scale ?? 1; + } + if (config.appearance.spacing) { + root.spacingScale = config.appearance.spacing.scale ?? 1; + } + if (config.appearance.transparency) { + root.transparencyEnabled = config.appearance.transparency.enabled ?? false; + root.transparencyBase = config.appearance.transparency.base ?? 0.85; + root.transparencyLayers = config.appearance.transparency.layers ?? 0.4; + } + } + // Update background settings if (config.background) { root.desktopClockEnabled = config.background.desktopClock?.enabled !== undefined ? config.background.desktopClock.enabled : false; @@ -85,6 +129,10 @@ RowLayout { if (exceptSection !== themeModeSection) themeModeSection.expanded = false; if (exceptSection !== colorVariantSection) colorVariantSection.expanded = false; if (exceptSection !== colorSchemeSection) colorSchemeSection.expanded = false; + if (exceptSection !== animationsSection) animationsSection.expanded = false; + if (exceptSection !== fontsSection) fontsSection.expanded = false; + if (exceptSection !== scalesSection) scalesSection.expanded = false; + if (exceptSection !== transparencySection) transparencySection.expanded = false; if (exceptSection !== backgroundSection) backgroundSection.expanded = false; } @@ -97,6 +145,37 @@ RowLayout { try { const config = JSON.parse(configFile.text()); + // Ensure appearance object exists + if (!config.appearance) config.appearance = {}; + + // Update animations + if (!config.appearance.anim) config.appearance.anim = {}; + if (!config.appearance.anim.durations) config.appearance.anim.durations = {}; + config.appearance.anim.durations.scale = root.animDurationsScale; + + // Update fonts + if (!config.appearance.font) config.appearance.font = {}; + if (!config.appearance.font.family) config.appearance.font.family = {}; + config.appearance.font.family.material = root.fontFamilyMaterial; + config.appearance.font.family.mono = root.fontFamilyMono; + config.appearance.font.family.sans = root.fontFamilySans; + if (!config.appearance.font.size) config.appearance.font.size = {}; + config.appearance.font.size.scale = root.fontSizeScale; + + // Update scales + if (!config.appearance.padding) config.appearance.padding = {}; + config.appearance.padding.scale = root.paddingScale; + if (!config.appearance.rounding) config.appearance.rounding = {}; + config.appearance.rounding.scale = root.roundingScale; + if (!config.appearance.spacing) config.appearance.spacing = {}; + config.appearance.spacing.scale = root.spacingScale; + + // Update transparency + if (!config.appearance.transparency) config.appearance.transparency = {}; + config.appearance.transparency.enabled = root.transparencyEnabled; + config.appearance.transparency.base = root.transparencyBase; + config.appearance.transparency.layers = root.transparencyLayers; + // Ensure background object exists if (!config.background) config.background = {}; @@ -602,6 +681,748 @@ RowLayout { } } + Item { + id: animationsSection + Layout.fillWidth: true + Layout.preferredHeight: animationsSectionHeader.implicitHeight + property bool expanded: false + + ColumnLayout { + id: animationsSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Animations") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: animationsSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = animationsSection.expanded; + root.collapseAllSections(animationsSection); + animationsSection.expanded = !wasExpanded; + } + } + } + } + + StyledRect { + visible: animationsSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: animationsSection.expanded ? animDurationsScaleRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: animDurationsScaleRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Animation duration scale") + } + + CustomSpinBox { + id: animDurationsScaleSpinBox + min: 0.1 + max: 5 + value: root.animDurationsScale + onValueModified: value => { + root.animDurationsScale = value; + root.saveConfig(); + } + } + } + } + + Item { + id: fontsSection + Layout.fillWidth: true + Layout.preferredHeight: fontsSectionHeader.implicitHeight + property bool expanded: false + + ColumnLayout { + id: fontsSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Fonts") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: fontsSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = fontsSection.expanded; + root.collapseAllSections(fontsSection); + fontsSection.expanded = !wasExpanded; + } + } + } + } + + StyledText { + visible: fontsSection.expanded + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Material font family") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledListView { + visible: fontsSection.expanded + Layout.fillWidth: true + implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 + Layout.topMargin: 0 + + Behavior on implicitHeight { + Anim {} + } + + model: Qt.fontFamilies() + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + delegate: StyledRect { + required property string modelData + + anchors.left: parent.left + anchors.right: parent.right + + readonly property bool isCurrent: modelData === root.fontFamilyMaterial + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + root.fontFamilyMaterial = modelData; + root.saveConfig(); + } + } + + RowLayout { + id: fontFamilyMaterialRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } + + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + } + } + + StyledText { + visible: fontsSection.expanded + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Monospace font family") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledListView { + visible: fontsSection.expanded + Layout.fillWidth: true + implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 + Layout.topMargin: 0 + + Behavior on implicitHeight { + Anim {} + } + + model: Qt.fontFamilies() + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + delegate: StyledRect { + required property string modelData + + anchors.left: parent.left + anchors.right: parent.right + + readonly property bool isCurrent: modelData === root.fontFamilyMono + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + root.fontFamilyMono = modelData; + root.saveConfig(); + } + } + + RowLayout { + id: fontFamilyMonoRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } + + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + } + } + + StyledText { + visible: fontsSection.expanded + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Sans-serif font family") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledListView { + visible: fontsSection.expanded + Layout.fillWidth: true + implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 + Layout.topMargin: 0 + + Behavior on implicitHeight { + Anim {} + } + + model: Qt.fontFamilies() + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + delegate: StyledRect { + required property string modelData + + anchors.left: parent.left + anchors.right: parent.right + + readonly property bool isCurrent: modelData === root.fontFamilySans + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + root.fontFamilySans = modelData; + root.saveConfig(); + } + } + + RowLayout { + id: fontFamilySansRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } + + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + } + } + + StyledRect { + visible: fontsSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: fontsSection.expanded ? fontSizeScaleRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: fontSizeScaleRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Font size scale") + } + + CustomSpinBox { + id: fontSizeScaleSpinBox + min: 0.1 + max: 5 + value: root.fontSizeScale + onValueModified: value => { + root.fontSizeScale = value; + root.saveConfig(); + } + } + } + } + + Item { + id: scalesSection + Layout.fillWidth: true + Layout.preferredHeight: scalesSectionHeader.implicitHeight + property bool expanded: false + + ColumnLayout { + id: scalesSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Scales") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: scalesSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = scalesSection.expanded; + root.collapseAllSections(scalesSection); + scalesSection.expanded = !wasExpanded; + } + } + } + } + + StyledRect { + visible: scalesSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: scalesSection.expanded ? paddingScaleRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: paddingScaleRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Padding scale") + } + + CustomSpinBox { + id: paddingScaleSpinBox + min: 0.1 + max: 5 + value: root.paddingScale + onValueModified: value => { + root.paddingScale = value; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: scalesSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: scalesSection.expanded ? roundingScaleRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: roundingScaleRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Rounding scale") + } + + CustomSpinBox { + id: roundingScaleSpinBox + min: 0.1 + max: 5 + value: root.roundingScale + onValueModified: value => { + root.roundingScale = value; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: scalesSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: scalesSection.expanded ? spacingScaleRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: spacingScaleRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Spacing scale") + } + + CustomSpinBox { + id: spacingScaleSpinBox + min: 0.1 + max: 5 + value: root.spacingScale + onValueModified: value => { + root.spacingScale = value; + root.saveConfig(); + } + } + } + } + + Item { + id: transparencySection + Layout.fillWidth: true + Layout.preferredHeight: transparencySectionHeader.implicitHeight + property bool expanded: false + + ColumnLayout { + id: transparencySectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Transparency") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: transparencySection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = transparencySection.expanded; + root.collapseAllSections(transparencySection); + transparencySection.expanded = !wasExpanded; + } + } + } + } + + StyledRect { + visible: transparencySection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: transparencySection.expanded ? transparencyEnabledRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: transparencyEnabledRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Transparency enabled") + } + + StyledSwitch { + id: transparencyEnabledSwitch + checked: root.transparencyEnabled + onToggled: { + root.transparencyEnabled = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: transparencySection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: transparencySection.expanded ? transparencyBaseRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: transparencyBaseRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Transparency base") + } + + CustomSpinBox { + id: transparencyBaseSpinBox + min: 0 + max: 1 + value: root.transparencyBase + onValueModified: value => { + root.transparencyBase = value; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: transparencySection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: transparencySection.expanded ? transparencyLayersRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: transparencyLayersRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Transparency layers") + } + + CustomSpinBox { + id: transparencyLayersSpinBox + min: 0 + max: 1 + value: root.transparencyLayers + onValueModified: value => { + root.transparencyLayers = value; + root.saveConfig(); + } + } + } + } + Item { id: backgroundSection Layout.fillWidth: true -- cgit v1.2.3-freya From e613ab81ccac640cb2bdfee0b48e9e8a61c749a5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 15:57:00 -0500 Subject: controlcenter: appearance panel, added border options --- .../controlcenter/appearance/AppearancePane.qml | 140 +++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 4bf6d44..d50eb58 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -33,6 +33,8 @@ RowLayout { property bool transparencyEnabled: false property real transparencyBase: 0.85 property real transparencyLayers: 0.4 + property real borderRounding: 1 + property real borderThickness: 1 // Background settings property bool desktopClockEnabled: true @@ -98,6 +100,12 @@ RowLayout { } } + // Update border settings + if (config.border) { + root.borderRounding = config.border.rounding ?? 1; + root.borderThickness = config.border.thickness ?? 1; + } + // Update background settings if (config.background) { root.desktopClockEnabled = config.background.desktopClock?.enabled !== undefined ? config.background.desktopClock.enabled : false; @@ -133,6 +141,7 @@ RowLayout { if (exceptSection !== fontsSection) fontsSection.expanded = false; if (exceptSection !== scalesSection) scalesSection.expanded = false; if (exceptSection !== transparencySection) transparencySection.expanded = false; + if (exceptSection !== borderSection) borderSection.expanded = false; if (exceptSection !== backgroundSection) backgroundSection.expanded = false; } @@ -193,6 +202,11 @@ RowLayout { config.background.visualiser.rounding = root.visualiserRounding; config.background.visualiser.spacing = root.visualiserSpacing; + // Update border + if (!config.border) config.border = {}; + config.border.rounding = root.borderRounding; + config.border.thickness = root.borderThickness; + // Write back to file using setText (same simple approach that worked for taskbar) const jsonString = JSON.stringify(config, null, 4); configFile.setText(jsonString); @@ -1423,6 +1437,132 @@ RowLayout { } } + Item { + id: borderSection + Layout.fillWidth: true + Layout.preferredHeight: borderSectionHeader.implicitHeight + property bool expanded: false + + ColumnLayout { + id: borderSectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Border") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: borderSection.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + const wasExpanded = borderSection.expanded; + root.collapseAllSections(borderSection); + borderSection.expanded = !wasExpanded; + } + } + } + } + + StyledRect { + visible: borderSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: borderSection.expanded ? borderRoundingRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: borderRoundingRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Border rounding") + } + + CustomSpinBox { + id: borderRoundingSpinBox + min: 0.1 + max: 5 + value: root.borderRounding + onValueModified: value => { + root.borderRounding = value; + root.saveConfig(); + } + } + } + } + + StyledRect { + visible: borderSection.expanded + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small / 2 + implicitHeight: borderSection.expanded ? borderThicknessRow.implicitHeight + Appearance.padding.large * 2 : 0 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: borderThicknessRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Border thickness") + } + + CustomSpinBox { + id: borderThicknessSpinBox + min: 0.1 + max: 5 + value: root.borderThickness + onValueModified: value => { + root.borderThickness = value; + root.saveConfig(); + } + } + } + } + Item { id: backgroundSection Layout.fillWidth: true -- cgit v1.2.3-freya From bcb2bd494ae469f0890838813132070663029fc6 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 16:15:10 -0500 Subject: controlcenter: truncating interface names if too long --- modules/controlcenter/ethernet/EthernetList.qml | 2 ++ modules/controlcenter/network/NetworkList.qml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/modules/controlcenter/ethernet/EthernetList.qml b/modules/controlcenter/ethernet/EthernetList.qml index 6ed50fd..ff7cee2 100644 --- a/modules/controlcenter/ethernet/EthernetList.qml +++ b/modules/controlcenter/ethernet/EthernetList.qml @@ -122,6 +122,8 @@ ColumnLayout { StyledText { Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 text: modelData.interface || qsTr("Unknown") } diff --git a/modules/controlcenter/network/NetworkList.qml b/modules/controlcenter/network/NetworkList.qml index f528092..60bd857 100644 --- a/modules/controlcenter/network/NetworkList.qml +++ b/modules/controlcenter/network/NetworkList.qml @@ -149,6 +149,8 @@ ColumnLayout { StyledText { Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 text: modelData.ssid || qsTr("Unknown") } -- cgit v1.2.3-freya From 5af1e9222e2f15c84102dc1ffb46e8643959e74a Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 16:18:32 -0500 Subject: controlcenter: added connection information to wireless to match ethernet panel --- modules/controlcenter/network/Details.qml | 101 ++++++++++++++++++++++++++ services/Network.qml | 114 +++++++++++++++++++++++++++++- 2 files changed, 214 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/network/Details.qml b/modules/controlcenter/network/Details.qml index 19e011f..31d20bc 100644 --- a/modules/controlcenter/network/Details.qml +++ b/modules/controlcenter/network/Details.qml @@ -16,6 +16,31 @@ Item { required property Session session readonly property var network: session.network.active + Component.onCompleted: { + if (network && network.active) { + Network.updateWirelessDeviceDetails(); + } + } + + onNetworkChanged: { + if (network && network.active) { + Network.updateWirelessDeviceDetails(); + } else { + Network.wirelessDeviceDetails = null; + } + } + + Connections { + target: Network + function onActiveChanged() { + if (root.network && root.network.active && Network.active && Network.active.ssid === root.network.ssid) { + Network.updateWirelessDeviceDetails(); + } else if (!root.network || !root.network.active) { + Network.wirelessDeviceDetails = null; + } + } + } + StyledFlickable { anchors.fill: parent @@ -198,6 +223,82 @@ Item { } } } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Connection information") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Network connection details") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: connectionInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: connectionInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("IP Address") + } + + StyledText { + text: Network.wirelessDeviceDetails?.ipAddress || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Subnet Mask") + } + + StyledText { + text: Network.wirelessDeviceDetails?.subnet || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Gateway") + } + + StyledText { + text: Network.wirelessDeviceDetails?.gateway || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("DNS Servers") + } + + StyledText { + text: (Network.wirelessDeviceDetails && Network.wirelessDeviceDetails.dns && Network.wirelessDeviceDetails.dns.length > 0) ? Network.wirelessDeviceDetails.dns.join(", ") : qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + wrapMode: Text.Wrap + Layout.maximumWidth: parent.width + } + } + } } } diff --git a/services/Network.qml b/services/Network.qml index acd4bcb..c8ab264 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -25,6 +25,7 @@ Singleton { property string ethernetDebugInfo: "" property bool ethernetProcessRunning: false property var ethernetDeviceDetails: null + property var wirelessDeviceDetails: null function enableWifi(enabled: bool): void { const cmd = enabled ? "on" : "off"; @@ -111,6 +112,11 @@ Singleton { } } + function updateWirelessDeviceDetails(): void { + // Find the wireless interface by looking for wifi devices + findWirelessInterfaceProc.exec(["nmcli", "device", "status"]); + } + function cidrToSubnetMask(cidr: string): string { // Convert CIDR notation (e.g., "24") to subnet mask (e.g., "255.255.255.0") const cidrNum = parseInt(cidr); @@ -587,7 +593,7 @@ Singleton { }; for (let i = 0; i < lines.length; i++) { - const line = lines[i]; +const line = lines[i]; const parts = line.split(":"); if (parts.length >= 2) { const key = parts[0].trim(); @@ -625,6 +631,112 @@ Singleton { } } + Process { + id: findWirelessInterfaceProc + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: StdioCollector { + onStreamFinished: { + const output = text.trim(); + if (!output || output.length === 0) { + root.wirelessDeviceDetails = null; + return; + } + + // Find the connected wifi interface from device status + const lines = output.split("\n"); + let wifiInterface = ""; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(/\s+/); + // Format: DEVICE TYPE STATE CONNECTION + // Look for wifi devices that are connected + if (parts.length >= 3 && parts[1] === "wifi" && parts[2] === "connected") { + wifiInterface = parts[0]; + break; + } + } + + if (wifiInterface && wifiInterface.length > 0) { + getWirelessDetailsProc.exec(["nmcli", "device", "show", wifiInterface]); + } else { + root.wirelessDeviceDetails = null; + } + } + } + onExited: { + if (exitCode !== 0) { + root.wirelessDeviceDetails = null; + } + } + } + + Process { + id: getWirelessDetailsProc + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: StdioCollector { + onStreamFinished: { + const output = text.trim(); + if (!output || output.length === 0) { + root.wirelessDeviceDetails = null; + return; + } + + const lines = output.split("\n"); + const details = { + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(":"); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join(":").trim(); + + if (key.startsWith("IP4.ADDRESS")) { + // Extract IP and subnet from format like "10.13.1.45/24" + const ipParts = value.split("/"); + details.ipAddress = ipParts[0] || ""; + if (ipParts[1]) { + // Convert CIDR notation to subnet mask + details.subnet = root.cidrToSubnetMask(ipParts[1]); + } else { + details.subnet = ""; + } + } else if (key === "IP4.GATEWAY") { + details.gateway = value; + } else if (key.startsWith("IP4.DNS")) { + details.dns.push(value); + } else if (key === "GENERAL.HWADDR") { + details.macAddress = value; + } + } + } + + root.wirelessDeviceDetails = details; + } + } + onExited: { + if (exitCode !== 0) { + root.wirelessDeviceDetails = null; + } + } + } + component AccessPoint: QtObject { required property var lastIpcObject readonly property string ssid: lastIpcObject.ssid -- cgit v1.2.3-freya From cb6eaaa25a61b0e14a3d2f120e4cbfef6551a1d0 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 16:22:14 -0500 Subject: controlcenter: desktop selector in appearance panel font styling --- .../controlcenter/appearance/AppearancePane.qml | 70 ++++++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index d50eb58..e3600ed 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -2045,16 +2045,76 @@ RowLayout { } } + // Gradient overlay for filename with rounded bottom corners + Rectangle { + id: filenameOverlay + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: filenameText.implicitHeight + Appearance.padding.normal * 2 + + // Match the parent's rounded corners at the bottom + radius: Appearance.rounding.normal + + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0) } + GradientStop { position: 0.3; color: Qt.rgba(0, 0, 0, 0.3) } + GradientStop { position: 0.7; color: Qt.rgba(0, 0, 0, 0.75) } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.85) } + } + + opacity: 0 + + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + + Component.onCompleted: { + opacity = 1; + } + } + StyledText { + id: filenameText anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: Appearance.padding.small + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + 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: modelData.relativePath - font.pointSize: Appearance.font.size.small - color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface - elide: Text.ElideRight + text: fileName + font.pointSize: Appearance.font.size.smaller + font.weight: 500 + color: isCurrent ? Colours.palette.m3primary : "#FFFFFF" + elide: Text.ElideMiddle + maximumLineCount: 1 + + // Text shadow for better readability + style: Text.Outline + styleColor: Qt.rgba(0, 0, 0, 0.6) + + opacity: 0 + + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + + Component.onCompleted: { + opacity = 1; + } } } } -- cgit v1.2.3-freya From b9ee123cb8a1ace8f683ce4b91ed19838aeb6efc Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 16:25:49 -0500 Subject: popout: audio and blueooth changed button to say "Open Settings" --- modules/bar/popouts/Audio.qml | 2 +- modules/bar/popouts/Bluetooth.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 55a30a2..0954b10 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -129,7 +129,7 @@ Item { StyledText { Layout.leftMargin: Appearance.padding.smaller - text: qsTr("Open panel") + text: qsTr("Open Settings") color: Colours.palette.m3onPrimaryContainer } diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 53d8b29..a43bba1 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -188,7 +188,7 @@ ColumnLayout { StyledText { Layout.leftMargin: Appearance.padding.smaller - text: qsTr("Open panel") + text: qsTr("Open Settings") color: Colours.palette.m3onPrimaryContainer } -- cgit v1.2.3-freya From 90bb8b1d006eaa4223c27754d621e6e26162003f Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 16:28:59 -0500 Subject: controlcenter: removed debug info toggles --- modules/controlcenter/launcher/LauncherPane.qml | 114 ------------------------ modules/controlcenter/taskbar/TaskbarPane.qml | 109 +--------------------- 2 files changed, 1 insertion(+), 222 deletions(-) diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 7377f42..82e145a 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -22,7 +22,6 @@ RowLayout { required property Session session property var selectedApp: null - property bool showDebugInfo: false anchors.fill: parent @@ -321,119 +320,6 @@ RowLayout { } } - StyledRect { - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal - implicitHeight: debugToggleRow.implicitHeight + Appearance.padding.large * 2 - color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal - - RowLayout { - id: debugToggleRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show debug information") - font.pointSize: Appearance.font.size.normal - } - - StyledSwitch { - id: showDebugInfoSwitch - checked: root.showDebugInfo - onToggled: { - root.showDebugInfo = checked; - } - } - } - } - - StyledRect { - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal - Layout.preferredHeight: 300 - visible: root.showDebugInfo - color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal - clip: true - - ColumnLayout { - anchors.fill: parent - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.small - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Debug Info - All Available Properties") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - StyledFlickable { - Layout.fillWidth: true - Layout.fillHeight: true - flickableDirection: Flickable.VerticalFlick - contentHeight: debugText.implicitHeight - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } - - TextEdit { - id: debugText - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - width: parent.width - - text: { - if (!root.selectedApp) return "No app selected"; - - let debug = ""; - const app = root.selectedApp; - const entry = app.entry; - - debug += "=== App Properties ===\n"; - for (let prop in app) { - try { - const value = app[prop]; - debug += prop + ": " + (value !== null && value !== undefined ? String(value) : "null/undefined") + "\n"; - } catch (e) { - debug += prop + ": [error accessing]\n"; - } - } - - debug += "\n=== Entry Properties ===\n"; - if (entry) { - for (let prop in entry) { - try { - const value = entry[prop]; - debug += prop + ": " + (value !== null && value !== undefined ? String(value) : "null/undefined") + "\n"; - } catch (e) { - debug += prop + ": [error accessing]\n"; - } - } - } else { - debug += "entry is null\n"; - } - - return debug; - } - font.pointSize: Appearance.font.size.small - color: Colours.palette.m3onSurfaceVariant - wrapMode: TextEdit.Wrap - readOnly: true - selectByMouse: true - selectByKeyboard: true - } - } - } - } } } } diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 105bbe1..22414c7 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -18,8 +18,6 @@ RowLayout { required property Session session - property bool showDebugInfo: false - // Bar Behavior property bool persistent: true property bool showOnHover: true @@ -120,8 +118,6 @@ RowLayout { function saveConfig(entryIndex, entryEnabled) { if (!configFile.loaded) { - root.lastSaveStatus = "Error: Config file not loaded yet"; - root.debugInfo = "Config file not loaded yet, cannot save"; return; } @@ -168,11 +164,6 @@ RowLayout { if (!config.bar.entries) config.bar.entries = []; config.bar.entries = []; - let debugInfo = `saveConfig called\n`; - debugInfo += `entryIndex: ${entryIndex}\n`; - debugInfo += `entryEnabled: ${entryEnabled}\n`; - debugInfo += `entriesModel.count: ${entriesModel.count}\n\n`; - for (let i = 0; i < entriesModel.count; i++) { const entry = entriesModel.get(i); // If this is the entry being updated, use the provided value (same as clock toggle reads from switch) @@ -180,9 +171,6 @@ RowLayout { let enabled = entry.enabled; if (entryIndex !== undefined && i === entryIndex) { enabled = entryEnabled; - debugInfo += `Entry ${i} (${entry.id}): Using provided value = ${entryEnabled}\n`; - } else { - debugInfo += `Entry ${i} (${entry.id}): Using model value = ${entry.enabled}\n`; } config.bar.entries.push({ id: entry.id, @@ -190,16 +178,11 @@ RowLayout { }); } - debugInfo += `\nFinal entries array:\n${JSON.stringify(config.bar.entries, null, 2)}\n`; - root.debugInfo = debugInfo; - // Write back to file using setText (same simple approach that worked for clock) const jsonString = JSON.stringify(config, null, 4); configFile.setText(jsonString); - root.lastSaveStatus = `Saved! Entries count: ${config.bar.entries.length}`; } catch (e) { - root.lastSaveStatus = `Error: ${e.message}`; - root.debugInfo = `Failed to save config:\n${e.message}\n${e.stack}`; + console.error("Failed to save config:", e); } } @@ -207,9 +190,6 @@ RowLayout { id: entriesModel } - // Debug info - property string debugInfo: "" - property string lastSaveStatus: "" function collapseAllSections(exceptSection) { if (exceptSection !== clockSection) clockSection.expanded = false; @@ -1261,93 +1241,6 @@ RowLayout { color: Colours.palette.m3outline } - StyledRect { - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.large - implicitHeight: debugToggleRow.implicitHeight + Appearance.padding.large * 2 - color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal - - RowLayout { - id: debugToggleRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show debug information") - font.pointSize: Appearance.font.size.normal - } - - StyledSwitch { - id: showDebugInfoSwitch - checked: root.showDebugInfo - onToggled: { - root.showDebugInfo = checked; - } - } - } - } - - StyledText { - Layout.topMargin: Appearance.spacing.large - Layout.alignment: Qt.AlignHCenter - visible: root.showDebugInfo - text: qsTr("Debug Info") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledRect { - Layout.fillWidth: true - Layout.preferredHeight: 200 - Layout.maximumHeight: 300 - visible: root.showDebugInfo - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - clip: true - - StyledFlickable { - id: debugFlickable - anchors.fill: parent - anchors.margins: Appearance.padding.normal - contentHeight: debugText.implicitHeight - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: debugFlickable - } - - TextEdit { - id: debugText - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - width: parent.width - - text: root.debugInfo || "No debug info yet" - font.pointSize: Appearance.font.size.small - color: Colours.palette.m3onSurface - wrapMode: TextEdit.Wrap - readOnly: true - selectByMouse: true - selectByKeyboard: true - } - } - } - - StyledText { - Layout.topMargin: Appearance.spacing.small - Layout.alignment: Qt.AlignHCenter - visible: root.showDebugInfo - text: root.lastSaveStatus || "" - font.pointSize: Appearance.font.size.small - color: root.lastSaveStatus.includes("Error") ? Colours.palette.m3error : Colours.palette.m3primary - } } } -- cgit v1.2.3-freya From 3e977f212d8c93f7dee96fafc871d33c0bbb922f Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 11 Nov 2025 16:33:21 -0500 Subject: tray: if background enabled and empty, background is hidden --- modules/bar/components/Tray.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index efd0c3a..96956f6 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -30,7 +30,7 @@ StyledRect { implicitWidth: Config.bar.sizes.innerWidth implicitHeight: nonAnimHeight - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.tray.background ? Colours.tPalette.m3surfaceContainer.a : 0) + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.full Column { -- cgit v1.2.3-freya From c1510b547645de5e8f70f6be99a0ba894b797241 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 14:51:22 -0500 Subject: notifs/toasts: reworked notifications and toasts and how they display and work together. see pull request comment. --- modules/drawers/Drawers.qml | 1 + modules/drawers/Interactions.qml | 46 +++++++ modules/drawers/Panels.qml | 88 +++++++++++++ modules/lock/NotifDock.qml | 4 +- modules/lock/NotifGroup.qml | 2 +- modules/notifications/Content.qml | 58 ++++++++- modules/notifications/Notification.qml | 67 +++++++--- modules/notifications/NotificationToast.qml | 154 ++++++++++++++++++++++ modules/notifications/NotificationToasts.qml | 186 +++++++++++++++++++++++++++ modules/notifications/Wrapper.qml | 2 + modules/utilities/toasts/ToastItem.qml | 9 +- services/Notifs.qml | 37 +++++- 12 files changed, 627 insertions(+), 27 deletions(-) create mode 100644 modules/notifications/NotificationToast.qml create mode 100644 modules/notifications/NotificationToasts.qml diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 2ba79a4..5337917 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -140,6 +140,7 @@ Variants { property bool dashboard property bool utilities property bool sidebar + property bool notifications Component.onCompleted: Visibilities.load(scope.modelData, this) } diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 9579b15..10190a4 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -198,6 +198,52 @@ CustomMouseArea { utilitiesShortcutActive = false; } + // Show notifications panel on hover + // Try using inTopPanel first (works when panel is visible), fallback to corner detection only when panel is collapsed + const panelHeight = panels.notifications.height || panels.notifications.implicitHeight || 0; + const panelWidth = panels.notifications.width || panels.notifications.implicitWidth || Config.notifs.sizes.width; + const panelX = bar.implicitWidth + panels.notifications.x; + const isPanelCollapsed = panelHeight < 10; // Consider collapsed if height is very small + + let showNotifications = inTopPanel(panels.notifications, x, y); + + // Only use fallback corner detection when panel is collapsed + if (!showNotifications && isPanelCollapsed) { + // Use panel's actual width and position for fallback, with some padding + const cornerPadding = Config.border.rounding || 20; + showNotifications = x >= panelX - cornerPadding && + x <= panelX + panelWidth + cornerPadding && + y < Config.border.thickness + cornerPadding; + } + + // Check if mouse is over the clear all button area + // Button is positioned to the left of the notification panel + if (!showNotifications && panels.notifications.height > 0 && panels.clearAllButton && panels.clearAllButton.visible) { + const buttonX = bar.implicitWidth + panels.clearAllButton.x; + const buttonY = Config.border.thickness + panels.clearAllButton.y; + const buttonWidth = panels.clearAllButton.width; + const buttonHeight = panels.clearAllButton.height; + + const inButtonArea = x >= buttonX && + x <= buttonX + buttonWidth && + y >= buttonY && + y <= buttonY + buttonHeight; + + if (inButtonArea) { + showNotifications = true; + } + } + + // Show or hide notification panel based on hover + if (panels.notifications.content) { + if (showNotifications) { + panels.notifications.content.show(); + } else { + // Hide if not hovering over panel or button + panels.notifications.content.shouldShow = false; + } + } + // Show popouts on hover if (x < bar.implicitWidth) { bar.checkPopout(y); diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 4ce1182..8b5a251 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -8,6 +8,10 @@ import qs.modules.bar.popouts as BarPopouts import qs.modules.utilities as Utilities import qs.modules.utilities.toasts as Toasts import qs.modules.sidebar as Sidebar +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services import Quickshell import QtQuick @@ -27,6 +31,7 @@ Item { readonly property alias utilities: utilities readonly property alias toasts: toasts readonly property alias sidebar: sidebar + readonly property alias clearAllButton: clearAllButton anchors.fill: parent anchors.margins: Config.border.thickness @@ -54,6 +59,89 @@ Item { anchors.right: parent.right } + // Clear all notifications button - positioned to the left of the notification panel + Item { + id: clearAllButton + + readonly property bool hasNotifications: Notifs.notClosed.length > 0 + readonly property bool panelVisible: notifications.height > 0 || notifications.implicitHeight > 0 + readonly property bool shouldShow: hasNotifications && panelVisible + + anchors.top: notifications.top + anchors.right: notifications.left + anchors.rightMargin: Appearance.padding.normal + anchors.topMargin: Appearance.padding.large + + width: button.implicitWidth + height: button.implicitHeight + enabled: shouldShow + + IconButton { + id: button + + icon: "clear_all" + radius: Appearance.rounding.normal + padding: Appearance.padding.normal + font.pointSize: Math.round(Appearance.font.size.large * 1.2) + + onClicked: { + // Clear all notifications + for (const notif of Notifs.list.slice()) + notif.close(); + } + + Elevation { + anchors.fill: parent + radius: parent.radius + z: -1 + level: button.stateLayer.containsMouse ? 4 : 3 + } + } + + // Keep notification panel visible when hovering over the button + MouseArea { + anchors.fill: button + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: { + if (notifications.content && Notifs.notClosed.length > 0) { + notifications.content.show(); + } + } + onExited: { + // Panel will be hidden by Interactions.qml if mouse is not over panel or button + } + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + opacity: shouldShow ? 1 : 0 + scale: shouldShow ? 1 : 0.5 + } + + Notifications.NotificationToasts { + id: notificationToasts + + panels: root + + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Config.border.thickness + anchors.rightMargin: Config.border.thickness + } + Session.Wrapper { id: session diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 7551e68..db087bd 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -22,7 +22,7 @@ ColumnLayout { StyledText { Layout.fillWidth: true - text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications") + text: Notifs.notClosed.length > 0 ? qsTr("%1 notification%2").arg(Notifs.notClosed.length).arg(Notifs.notClosed.length === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline font.family: Appearance.font.family.mono font.weight: 500 @@ -42,7 +42,7 @@ ColumnLayout { anchors.centerIn: parent asynchronous: true active: opacity > 0 - opacity: Notifs.list.length > 0 ? 0 : 1 + opacity: Notifs.notClosed.length > 0 ? 0 : 1 sourceComponent: ColumnLayout { spacing: Appearance.spacing.large diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 2a08c26..50b14ae 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -16,7 +16,7 @@ StyledRect { required property string modelData - readonly property list notifs: Notifs.list.filter(notif => notif.appName === modelData) + readonly property list notifs: Notifs.notClosed.filter(notif => notif.appName === modelData) readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? "" readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low" diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index 2d4590e..035a228 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -13,6 +13,8 @@ Item { required property Item panels readonly property int padding: Appearance.padding.large + property bool shouldShow: false + anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right @@ -20,13 +22,16 @@ Item { implicitWidth: Config.notifs.sizes.width + padding * 2 implicitHeight: { const count = list.count; - if (count === 0) + if (count === 0 || !shouldShow) return 0; let height = (count - 1) * Appearance.spacing.smaller; for (let i = 0; i < count; i++) height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; + const screenHeight = QsWindow.window?.screen?.height ?? 0; + const maxHeight = Math.floor(screenHeight * 0.45); + if (visibilities && panels) { if (visibilities.osd) { const h = panels.osd.y - Config.border.rounding * 2 - padding * 2; @@ -41,7 +46,8 @@ Item { } } - return Math.min((QsWindow.window?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); + const availableHeight = Math.min(maxHeight, screenHeight - Config.border.thickness * 2); + return Math.min(availableHeight, height + padding * 2); } ClippingWrapperRectangle { @@ -55,7 +61,7 @@ Item { id: list model: ScriptModel { - values: Notifs.popups.filter(n => !n.closed) + values: [...Notifs.notClosed] } anchors.fill: parent @@ -192,6 +198,52 @@ Item { } } + Timer { + id: hideTimer + + interval: 5000 + onTriggered: { + if (list.count > 0) + root.shouldShow = false; + } + } + + function show(): void { + if (list.count > 0) { + shouldShow = true; + hideTimer.restart(); + } + } + + Connections { + target: list + + function onCountChanged(): void { + if (list.count === 0) { + root.shouldShow = false; + hideTimer.stop(); + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: { + if (list.count > 0) { + root.shouldShow = true; + hideTimer.restart(); + } + } + onExited: { + if (list.count > 0) { + root.shouldShow = false; + hideTimer.stop(); + } + } + } + Behavior on implicitHeight { Anim {} } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 95507fc..091da2c 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -17,22 +17,35 @@ StyledRect { required property Notifs.Notif modelData readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 - readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 + readonly property bool isCritical: modelData.urgency === NotificationUrgency.Critical + readonly property bool isLow: modelData.urgency === NotificationUrgency.Low + readonly property int nonAnimHeight: { + const baseHeight = summary.implicitHeight + inner.anchors.margins * 2; + return root.expanded + ? baseHeight + appName.height + body.height + actions.height + actions.anchors.topMargin + : baseHeight + bodyPreview.height; + } property bool expanded + property bool disableSlideIn: false - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer + color: root.isCritical + ? Colours.palette.m3secondaryContainer + : Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.normal implicitWidth: Config.notifs.sizes.width implicitHeight: inner.implicitHeight - x: Config.notifs.sizes.width + x: disableSlideIn ? 0 : Config.notifs.sizes.width Component.onCompleted: { - x = 0; + if (!root.disableSlideIn) { + x = 0; + } modelData.lock(this); } Component.onDestruction: modelData.unlock(this) Behavior on x { + enabled: !disableSlideIn Anim { easing.bezierCurve: Appearance.anim.curves.emphasizedDecel } @@ -134,8 +147,8 @@ StyledRect { Loader { id: appIcon - active: root.hasAppIcon || !root.hasImage - asynchronous: true + active: !root.hasImage || root.hasAppIcon + asynchronous: false anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter @@ -144,7 +157,11 @@ StyledRect { sourceComponent: StyledRect { radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer + color: { + if (root.isCritical) return Colours.palette.m3error; + if (root.isLow) return Colours.layer(Colours.palette.m3surfaceContainerHighest, 2); + return Colours.palette.m3secondaryContainer; + } implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image @@ -152,7 +169,8 @@ StyledRect { id: icon active: root.hasAppIcon - asynchronous: true + asynchronous: false + visible: active anchors.centerIn: parent @@ -162,14 +180,19 @@ StyledRect { sourceComponent: ColouredIcon { anchors.fill: parent source: Quickshell.iconPath(root.modelData.appIcon) - colour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + colour: { + if (root.isCritical) return Colours.palette.m3onError; + if (root.isLow) return Colours.palette.m3onSurface; + return Colours.palette.m3onSecondaryContainer; + } layer.enabled: root.modelData.appIcon.endsWith("symbolic") } } Loader { active: !root.hasAppIcon - asynchronous: true + asynchronous: false + visible: active anchors.centerIn: parent anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 @@ -177,7 +200,11 @@ StyledRect { sourceComponent: MaterialIcon { text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + color: { + if (root.isCritical) return Colours.palette.m3onError; + if (root.isLow) return Colours.palette.m3onSurface; + return Colours.palette.m3onSecondaryContainer; + } font.pointSize: Appearance.font.size.large } } @@ -322,7 +349,9 @@ StyledRect { StateLayer { radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + color: root.isCritical + ? Colours.palette.m3onSecondaryContainer + : Colours.palette.m3onSurface function onClicked() { root.expanded = !root.expanded; @@ -442,8 +471,12 @@ StyledRect { required property var modelData + readonly property bool isCritical: root.isCritical + radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + color: isCritical + ? Colours.palette.m3secondary + : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2 Layout.preferredHeight: actionText.height + Appearance.padding.small * 2 @@ -452,7 +485,9 @@ StyledRect { StateLayer { radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface + color: isCritical + ? Colours.palette.m3onSecondary + : Colours.palette.m3onSurface function onClicked(): void { action.modelData.invoke(); @@ -464,7 +499,9 @@ StyledRect { anchors.centerIn: parent text: actionTextMetrics.elidedText - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant + color: isCritical + ? Colours.palette.m3onSecondary + : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } diff --git a/modules/notifications/NotificationToast.qml b/modules/notifications/NotificationToast.qml new file mode 100644 index 0000000..1ce334b --- /dev/null +++ b/modules/notifications/NotificationToast.qml @@ -0,0 +1,154 @@ +import qs.components +import qs.components.effects +import qs.services +import qs.config +import qs.utils +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property Notifs.Notif modelData + + readonly property bool hasImage: modelData.image.length > 0 + readonly property bool hasAppIcon: modelData.appIcon.length > 0 + + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.normal + color: Colours.palette.m3surface + + border.width: 1 + border.color: Colours.palette.m3outlineVariant + + Elevation { + anchors.fill: parent + radius: parent.radius + opacity: parent.opacity + z: -1 + level: 3 + } + + RowLayout { + id: layout + + anchors.fill: parent + anchors.margins: Appearance.padding.smaller + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + spacing: Appearance.spacing.normal + + Item { + Layout.preferredWidth: Config.notifs.sizes.image + Layout.preferredHeight: Config.notifs.sizes.image + + Loader { + id: imageLoader + + active: root.hasImage + asynchronous: true + anchors.fill: parent + + sourceComponent: ClippingRectangle { + radius: Appearance.rounding.full + implicitWidth: Config.notifs.sizes.image + implicitHeight: Config.notifs.sizes.image + + Image { + anchors.fill: parent + source: Qt.resolvedUrl(root.modelData.image) + fillMode: Image.PreserveAspectCrop + cache: false + asynchronous: true + } + } + } + + Loader { + id: appIconLoader + + active: root.hasAppIcon || !root.hasImage + asynchronous: true + + anchors.horizontalCenter: root.hasImage ? undefined : parent.horizontalCenter + anchors.verticalCenter: root.hasImage ? undefined : parent.verticalCenter + anchors.right: root.hasImage ? parent.right : undefined + anchors.bottom: root.hasImage ? parent.bottom : undefined + + sourceComponent: StyledRect { + radius: Appearance.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer + implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + + Loader { + id: appIcon + + active: root.hasAppIcon + asynchronous: true + + anchors.centerIn: parent + + width: Math.round(parent.width * 0.6) + height: Math.round(parent.width * 0.6) + + sourceComponent: ColouredIcon { + anchors.fill: parent + source: Quickshell.iconPath(root.modelData.appIcon) + colour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + layer.enabled: root.modelData.appIcon.endsWith("symbolic") + } + } + + Loader { + active: !root.hasAppIcon + asynchronous: true + anchors.centerIn: parent + anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 + anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 + + sourceComponent: MaterialIcon { + text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + font.pointSize: Appearance.font.size.large + } + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + id: title + + Layout.fillWidth: true + text: root.modelData.summary + color: Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.normal + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + textFormat: Text.StyledText + text: root.modelData.body + color: Colours.palette.m3onSurface + opacity: 0.8 + elide: Text.ElideRight + } + } + } + + Behavior on border.color { + CAnim {} + } +} diff --git a/modules/notifications/NotificationToasts.qml b/modules/notifications/NotificationToasts.qml new file mode 100644 index 0000000..96fe817 --- /dev/null +++ b/modules/notifications/NotificationToasts.qml @@ -0,0 +1,186 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.config +import qs.services +import Quickshell +import Quickshell.Widgets +import QtQuick + +Item { + id: root + + required property Item panels + + readonly property int spacing: Appearance.spacing.small + readonly property int maxToasts: 5 + readonly property bool listVisible: panels.notifications.content.shouldShow + + property bool flag + property var activeToasts: new Set() + + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Appearance.padding.normal + + implicitWidth: Config.notifs.sizes.width + implicitHeight: { + if (listVisible) + return 0; + + let height = -spacing; + for (let i = 0; i < repeater.count; i++) { + const item = repeater.itemAt(i) as ToastWrapper; + if (item && !item.modelData.closed && !item.previewHidden) + height += item.implicitHeight + spacing; + } + return height; + } + + opacity: listVisible ? 0 : 1 + visible: opacity > 0 + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + } + } + + Repeater { + id: repeater + + model: ScriptModel { + values: { + const toasts = []; + let visibleCount = 0; + + for (const notif of Notifs.list) { + if (notif.showAsToast) { + root.activeToasts.add(notif); + } + if (notif.closed) { + root.activeToasts.delete(notif); + } + } + + for (const notif of Notifs.list) { + if (root.activeToasts.has(notif)) { + toasts.push(notif); + if (notif.showAsToast && !notif.closed) { + visibleCount++; + if (visibleCount > root.maxToasts) + break; + } + } + } + return toasts; + } + onValuesChanged: root.flagChanged() + } + + ToastWrapper {} + } + + component ToastWrapper: MouseArea { + id: toast + + required property int index + required property Notifs.Notif modelData + + readonly property bool previewHidden: { + let extraHidden = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i); + if (item && item.modelData.closed) + extraHidden++; + } + return index >= root.maxToasts + extraHidden; + } + + opacity: modelData.closed || previewHidden || !modelData.showAsToast ? 0 : 1 + scale: modelData.closed || previewHidden || !modelData.showAsToast ? 0.7 : 1 + + anchors.topMargin: { + root.flag; + let margin = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i) as ToastWrapper; + if (item && !item.modelData.closed && !item.previewHidden) + margin += item.implicitHeight + root.spacing; + } + return margin; + } + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + implicitHeight: toastInner.implicitHeight + + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton + onClicked: { + modelData.showAsToast = false; + modelData.close(); + } + + Component.onCompleted: modelData.lock(this) + + onPreviewHiddenChanged: { + if (initAnim.running && previewHidden) + initAnim.stop(); + } + + Anim { + id: initAnim + + Component.onCompleted: running = !toast.previewHidden + + target: toast + properties: "opacity,scale" + from: 0 + to: 1 + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + + ParallelAnimation { + running: toast.modelData.closed || (!toast.modelData.showAsToast && !toast.modelData.closed) + onStarted: toast.anchors.topMargin = toast.anchors.topMargin + onFinished: { + if (toast.modelData.closed) + toast.modelData.unlock(toast); + } + + Anim { + target: toast + property: "opacity" + to: 0 + } + Anim { + target: toast + property: "scale" + to: 0.7 + } + } + + NotificationToast { + id: toastInner + + modelData: toast.modelData + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + Behavior on anchors.topMargin { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + } +} diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 61acc56..4b54883 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -8,6 +8,8 @@ Item { required property var visibilities required property Item panels + readonly property alias content: content + visible: height > 0 implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) implicitHeight: content.implicitHeight diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index f475500..477a23c 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -28,14 +28,13 @@ StyledRect { border.width: 1 border.color: { - let colour = Colours.palette.m3outlineVariant; if (root.modelData.type === Toast.Success) - colour = Colours.palette.m3success; + return Colours.palette.m3success; if (root.modelData.type === Toast.Warning) - colour = Colours.palette.m3secondaryContainer; + return Colours.palette.m3secondaryContainer; if (root.modelData.type === Toast.Error) - colour = Colours.palette.m3error; - return Qt.alpha(colour, 0.3); + return Colours.palette.m3error; + return Colours.palette.m3outlineVariant; } Elevation { diff --git a/services/Notifs.qml b/services/Notifs.qml index 4a89c7f..ea0c52a 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -77,8 +77,10 @@ Singleton { onNotification: notif => { notif.tracked = true; + const shouldShowAsToast = !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar); const comp = notifComp.createObject(root, { - popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), + popup: shouldShowAsToast, + showAsToast: shouldShowAsToast, notification: notif }); root.list = [comp, ...root.list]; @@ -143,6 +145,7 @@ Singleton { property bool popup property bool closed + property bool showAsToast: false property var locks: new Set() property date time: new Date() @@ -177,6 +180,38 @@ Singleton { property list actions readonly property Timer timer: Timer { + id: toastTimer + + running: notif.showAsToast + interval: { + let timeout = notif.expireTimeout; + if (timeout <= 0) { + switch (notif.urgency) { + case NotificationUrgency.Critical: + timeout = 10000; + break; + case NotificationUrgency.Normal: + timeout = 5000; + break; + case NotificationUrgency.Low: + timeout = 5000; + break; + default: + timeout = 5000; + } + } + return timeout; + } + onTriggered: { + if (Config.notifs.expire) + notif.popup = false; + if (notif.showAsToast) { + notif.showAsToast = false; + } + } + } + + readonly property Timer popupTimer: Timer { running: true interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout onTriggered: { -- cgit v1.2.3-freya From e92187293e4afa046ca05bd80796c1fa193097e5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 15:58:42 -0500 Subject: controlcenter: refactoring into components --- components/controls/CollapsibleSection.qml | 85 + components/controls/SpinBoxRow.qml | 51 + components/controls/SwitchRow.qml | 47 + components/controls/ToggleButton.qml | 82 + .../controlcenter/appearance/AppearancePane.qml | 1872 ++++++-------------- modules/controlcenter/ethernet/EthernetList.qml | 77 +- modules/controlcenter/network/NetworkList.qml | 81 +- modules/controlcenter/taskbar/TaskbarPane.qml | 1013 +++-------- 8 files changed, 1014 insertions(+), 2294 deletions(-) create mode 100644 components/controls/CollapsibleSection.qml create mode 100644 components/controls/SpinBoxRow.qml create mode 100644 components/controls/SwitchRow.qml create mode 100644 components/controls/ToggleButton.qml diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml new file mode 100644 index 0000000..945386c --- /dev/null +++ b/components/controls/CollapsibleSection.qml @@ -0,0 +1,85 @@ +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property string title + property string description: "" + property bool expanded: false + + signal toggleRequested + + spacing: Appearance.spacing.small / 2 + Layout.fillWidth: true + + Item { + id: sectionHeaderItem + Layout.fillWidth: true + Layout.preferredHeight: sectionHeader.implicitHeight + + ColumnLayout { + id: sectionHeader + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: root.title + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: root.expanded ? 180 : 0 + color: Colours.palette.m3onSurface + Behavior on rotation { + Anim {} + } + } + } + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Appearance.padding.normal + anchors.rightMargin: -Appearance.padding.normal + function onClicked(): void { + root.toggleRequested(); + root.expanded = !root.expanded; + } + } + + StyledText { + visible: root.expanded && root.description !== "" + text: root.description + color: Colours.palette.m3outline + Layout.fillWidth: true + } + } + } + + default property alias content: contentColumn.data + + ColumnLayout { + id: contentColumn + Layout.fillWidth: true + visible: root.expanded + spacing: Appearance.spacing.small / 2 + } +} + diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml new file mode 100644 index 0000000..a4441c5 --- /dev/null +++ b/components/controls/SpinBoxRow.qml @@ -0,0 +1,51 @@ +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property string label + required property real value + required property real min + required property real max + property var onValueModified: function(value) {} + + Layout.fillWidth: true + implicitHeight: row.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + } + + CustomSpinBox { + min: root.min + max: root.max + value: root.value + onValueModified: value => { + root.onValueModified(value); + } + } + } +} + diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml new file mode 100644 index 0000000..a486ee2 --- /dev/null +++ b/components/controls/SwitchRow.qml @@ -0,0 +1,47 @@ +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property string label + required property bool checked + property var onToggled: function(checked) {} + + Layout.fillWidth: true + implicitHeight: row.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + } + + StyledSwitch { + checked: root.checked + onToggled: { + root.onToggled(checked); + } + } + } +} + diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml new file mode 100644 index 0000000..9d8e094 --- /dev/null +++ b/components/controls/ToggleButton.qml @@ -0,0 +1,82 @@ +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property bool toggled + property string icon + property string label + property string accent: "Secondary" + + signal clicked + + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) + implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2 + implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) + color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] + + StateLayer { + id: toggleStateLayer + + color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] + + function onClicked(): void { + root.clicked(); + } + } + + RowLayout { + id: toggleBtnInner + + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + id: toggleBtnIcon + + visible: !!text + fill: root.toggled ? 1 : 0 + text: root.icon + color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] + font.pointSize: Appearance.font.size.large + + Behavior on fill { + Anim {} + } + } + + Loader { + asynchronous: true + active: !!root.label + visible: active + + sourceComponent: StyledText { + text: root.label + color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] + } + } + } + + Behavior on radius { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + Behavior on Layout.preferredWidth { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } +} + diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index e3600ed..cfe5b56 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -255,1588 +255,770 @@ RowLayout { } } - Item { + CollapsibleSection { id: themeModeSection - Layout.fillWidth: true - Layout.preferredHeight: themeModeSectionHeader.implicitHeight - property bool expanded: false - - ColumnLayout { - id: themeModeSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Theme mode") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - MaterialIcon { - text: "expand_more" - rotation: themeModeSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} - } - } - } - - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = themeModeSection.expanded; - root.collapseAllSections(themeModeSection); - themeModeSection.expanded = !wasExpanded; - } - } - - StyledText { - visible: themeModeSection.expanded - text: qsTr("Light or dark theme") - color: Colours.palette.m3outline - Layout.fillWidth: true - } - } - } - - StyledRect { - visible: themeModeSection.expanded - Layout.fillWidth: true - implicitHeight: themeModeSection.expanded ? modeToggle.implicitHeight + Appearance.padding.large * 2 : 0 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} + title: qsTr("Theme mode") + description: qsTr("Light or dark theme") + onToggleRequested: { + root.collapseAllSections(themeModeSection); } - RowLayout { - id: modeToggle - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.normal + StyledRect { + Layout.fillWidth: true + implicitHeight: modeToggle.implicitHeight + Appearance.padding.large * 2 - StyledText { - Layout.fillWidth: true - text: qsTr("Dark mode") - } + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - StyledSwitch { - checked: !Colours.currentLight - onToggled: { - Colours.setMode(checked ? "dark" : "light"); - } + Behavior on implicitHeight { + Anim {} } - } - } - Item { - id: colorVariantSection - Layout.fillWidth: true - Layout.preferredHeight: colorVariantSectionHeader.implicitHeight - property bool expanded: false + RowLayout { + id: modeToggle - ColumnLayout { - id: colorVariantSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Appearance.spacing.normal StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Color variant") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { Layout.fillWidth: true + text: qsTr("Dark mode") } - MaterialIcon { - text: "expand_more" - rotation: colorVariantSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} + StyledSwitch { + checked: !Colours.currentLight + onToggled: { + Colours.setMode(checked ? "dark" : "light"); } } } - - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = colorVariantSection.expanded; - root.collapseAllSections(colorVariantSection); - colorVariantSection.expanded = !wasExpanded; - } - } - - StyledText { - visible: colorVariantSection.expanded - text: qsTr("Material theme variant") - color: Colours.palette.m3outline - Layout.fillWidth: true - } } } - StyledListView { - visible: colorVariantSection.expanded - Layout.fillWidth: true - implicitHeight: colorVariantSection.expanded ? Math.min(400, M3Variants.list.length * 60) : 0 - Layout.topMargin: 0 - - Behavior on implicitHeight { - Anim {} + CollapsibleSection { + id: colorVariantSection + title: qsTr("Color variant") + description: qsTr("Material theme variant") + onToggleRequested: { + root.collapseAllSections(colorVariantSection); } - model: M3Variants.list - spacing: Appearance.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } + StyledListView { + Layout.fillWidth: true + implicitHeight: colorVariantSection.expanded ? Math.min(400, M3Variants.list.length * 60) : 0 - delegate: StyledRect { - required property var modelData + Behavior on implicitHeight { + Anim {} + } - anchors.left: parent.left - anchors.right: parent.right + model: M3Variants.list + spacing: Appearance.spacing.small / 2 + clip: true - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - const variant = modelData.variant; - - // Optimistic update - set immediately - Schemes.currentVariant = variant; - - // Execute the command - Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); - - // Reload after a delay to confirm - Qt.callLater(() => { - reloadTimer.restart(); - }); - } - } - - Timer { - id: reloadTimer - interval: 300 - onTriggered: { - Schemes.reload(); - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent } - RowLayout { - id: variantRow + delegate: StyledRect { + required property var modelData anchors.left: parent.left anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal - - MaterialIcon { - text: modelData.icon - font.pointSize: Appearance.font.size.large - fill: modelData.variant === Schemes.currentVariant ? 1 : 0 - } + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 + border.color: Colours.palette.m3primary - StyledText { - Layout.fillWidth: true - text: modelData.name - font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 + StateLayer { + function onClicked(): void { + const variant = modelData.variant; + + // Optimistic update - set immediately + Schemes.currentVariant = variant; + + // Execute the command + Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); + + // Reload after a delay to confirm + Qt.callLater(() => { + reloadTimer.restart(); + }); + } } - - MaterialIcon { - visible: modelData.variant === Schemes.currentVariant - text: "check" - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large + + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); + } } - } - implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 - } - } + RowLayout { + id: variantRow - Item { - id: colorSchemeSection - Layout.fillWidth: true - Layout.preferredHeight: colorSchemeSectionHeader.implicitHeight - property bool expanded: false - - ColumnLayout { - id: colorSchemeSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Appearance.spacing.normal - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Color scheme") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } + MaterialIcon { + text: modelData.icon + font.pointSize: Appearance.font.size.large + fill: modelData.variant === Schemes.currentVariant ? 1 : 0 + } - MaterialIcon { - text: "expand_more" - rotation: colorSchemeSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} + StyledText { + Layout.fillWidth: true + text: modelData.name + font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 } - } - } - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = colorSchemeSection.expanded; - root.collapseAllSections(colorSchemeSection); - colorSchemeSection.expanded = !wasExpanded; + MaterialIcon { + visible: modelData.variant === Schemes.currentVariant + text: "check" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.large + } } - } - StyledText { - visible: colorSchemeSection.expanded - text: qsTr("Available color schemes") - color: Colours.palette.m3outline - Layout.fillWidth: true + implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 } } } - StyledListView { - visible: colorSchemeSection.expanded - Layout.fillWidth: true - implicitHeight: colorSchemeSection.expanded ? Math.min(400, Schemes.list.length * 80) : 0 - Layout.topMargin: 0 - - Behavior on implicitHeight { - Anim {} - } - - model: Schemes.list - spacing: Appearance.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent + CollapsibleSection { + id: colorSchemeSection + title: qsTr("Color scheme") + description: qsTr("Available color schemes") + onToggleRequested: { + root.collapseAllSections(colorSchemeSection); } - delegate: StyledRect { - required property var modelData + StyledListView { + Layout.fillWidth: true + implicitHeight: colorSchemeSection.expanded ? Math.min(400, Schemes.list.length * 80) : 0 - anchors.left: parent.left - anchors.right: parent.right + Behavior on implicitHeight { + Anim {} + } - readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` - readonly property bool isCurrent: schemeKey === Schemes.currentScheme + model: Schemes.list + spacing: Appearance.spacing.small / 2 + clip: true - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - const name = modelData.name; - const flavour = modelData.flavour; - const schemeKey = `${name} ${flavour}`; - - // Optimistic update - set immediately - Schemes.currentScheme = schemeKey; - - // Execute the command - Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - - // Reload after a delay to confirm - Qt.callLater(() => { - reloadTimer.restart(); - }); - } - } - - Timer { - id: reloadTimer - interval: 300 - onTriggered: { - Schemes.reload(); - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent } - RowLayout { - id: schemeRow + delegate: StyledRect { + required property var modelData anchors.left: parent.left anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` + readonly property bool isCurrent: schemeKey === Schemes.currentScheme + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + const name = modelData.name; + const flavour = modelData.flavour; + const schemeKey = `${name} ${flavour}`; + + // Optimistic update - set immediately + Schemes.currentScheme = schemeKey; + + // Execute the command + Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); + + // Reload after a delay to confirm + Qt.callLater(() => { + reloadTimer.restart(); + }); + } + } + + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); + } + } - StyledRect { - id: preview + RowLayout { + id: schemeRow + anchors.left: parent.left + anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - border.width: 1 - border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) + spacing: Appearance.spacing.normal - color: `#${modelData.colours?.surface}` - radius: Appearance.rounding.full - implicitWidth: iconPlaceholder.implicitWidth - implicitHeight: iconPlaceholder.implicitWidth + StyledRect { + id: preview - MaterialIcon { - id: iconPlaceholder - visible: false - text: "circle" - font.pointSize: Appearance.font.size.large - } + anchors.verticalCenter: parent.verticalCenter - Item { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right + border.width: 1 + border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) - implicitWidth: parent.implicitWidth / 2 - clip: true + color: `#${modelData.colours?.surface}` + radius: Appearance.rounding.full + implicitWidth: iconPlaceholder.implicitWidth + implicitHeight: iconPlaceholder.implicitWidth - StyledRect { + MaterialIcon { + id: iconPlaceholder + visible: false + text: "circle" + font.pointSize: Appearance.font.size.large + } + + Item { anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right - implicitWidth: preview.implicitWidth - color: `#${modelData.colours?.primary}` - radius: Appearance.rounding.full - } - } - } + implicitWidth: parent.implicitWidth / 2 + clip: true - Column { - Layout.fillWidth: true - spacing: 0 + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right - StyledText { - text: modelData.flavour ?? "" - font.pointSize: Appearance.font.size.normal + implicitWidth: preview.implicitWidth + color: `#${modelData.colours?.primary}` + radius: Appearance.rounding.full + } + } } - StyledText { - text: modelData.name ?? "" - font.pointSize: Appearance.font.size.small - color: Colours.palette.m3outline + Column { + Layout.fillWidth: true + spacing: 0 - elide: Text.ElideRight - anchors.left: parent.left - anchors.right: parent.right - } - } + StyledText { + text: modelData.flavour ?? "" + font.pointSize: Appearance.font.size.normal + } - Loader { - active: isCurrent - asynchronous: true + StyledText { + text: modelData.name ?? "" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + elide: Text.ElideRight + anchors.left: parent.left + anchors.right: parent.right + } } - } - } - - implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 - } - } - Item { - id: animationsSection - Layout.fillWidth: true - Layout.preferredHeight: animationsSectionHeader.implicitHeight - property bool expanded: false - - ColumnLayout { - id: animationsSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + Loader { + active: isCurrent + asynchronous: true - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Animations") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - MaterialIcon { - text: "expand_more" - rotation: animationsSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = animationsSection.expanded; - root.collapseAllSections(animationsSection); - animationsSection.expanded = !wasExpanded; - } + implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 } } } - StyledRect { - visible: animationsSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: animationsSection.expanded ? animDurationsScaleRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} + CollapsibleSection { + id: animationsSection + title: qsTr("Animations") + onToggleRequested: { + root.collapseAllSections(animationsSection); } - RowLayout { - id: animDurationsScaleRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Animation duration scale") - } - - CustomSpinBox { - id: animDurationsScaleSpinBox - min: 0.1 - max: 5 - value: root.animDurationsScale - onValueModified: value => { - root.animDurationsScale = value; - root.saveConfig(); - } + SpinBoxRow { + label: qsTr("Animation duration scale") + min: 0.1 + max: 5 + value: root.animDurationsScale + onValueModified: value => { + root.animDurationsScale = value; + root.saveConfig(); } } } - Item { + CollapsibleSection { id: fontsSection - Layout.fillWidth: true - Layout.preferredHeight: fontsSectionHeader.implicitHeight - property bool expanded: false - - ColumnLayout { - id: fontsSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Fonts") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - MaterialIcon { - text: "expand_more" - rotation: fontsSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} - } - } - } - - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = fontsSection.expanded; - root.collapseAllSections(fontsSection); - fontsSection.expanded = !wasExpanded; - } - } + title: qsTr("Fonts") + onToggleRequested: { + root.collapseAllSections(fontsSection); } - } - StyledText { - visible: fontsSection.expanded - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Material font family") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledListView { - visible: fontsSection.expanded - Layout.fillWidth: true - implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - Layout.topMargin: 0 - - Behavior on implicitHeight { - Anim {} - } - - model: Qt.fontFamilies() - spacing: Appearance.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Material font family") + font.pointSize: Appearance.font.size.larger + font.weight: 500 } - delegate: StyledRect { - required property string modelData + StyledListView { + Layout.fillWidth: true + implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - anchors.left: parent.left - anchors.right: parent.right + Behavior on implicitHeight { + Anim {} + } - readonly property bool isCurrent: modelData === root.fontFamilyMaterial - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + model: Qt.fontFamilies() + spacing: Appearance.spacing.small / 2 + clip: true - StateLayer { - function onClicked(): void { - root.fontFamilyMaterial = modelData; - root.saveConfig(); - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent } - RowLayout { - id: fontFamilyMaterialRow + delegate: StyledRect { + required property string modelData anchors.left: parent.left anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal - - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } + readonly property bool isCurrent: modelData === root.fontFamilyMaterial + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - Loader { - active: isCurrent - asynchronous: true - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + StateLayer { + function onClicked(): void { + root.fontFamilyMaterial = modelData; + root.saveConfig(); } } - } - - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 - } - } - - StyledText { - visible: fontsSection.expanded - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Monospace font family") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledListView { - visible: fontsSection.expanded - Layout.fillWidth: true - implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - Layout.topMargin: 0 - - Behavior on implicitHeight { - Anim {} - } - - model: Qt.fontFamilies() - spacing: Appearance.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } - - delegate: StyledRect { - required property string modelData - - anchors.left: parent.left - anchors.right: parent.right - - readonly property bool isCurrent: modelData === root.fontFamilyMono - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - root.fontFamilyMono = modelData; - root.saveConfig(); - } - } - RowLayout { - id: fontFamilyMonoRow + RowLayout { + id: fontFamilyMaterialRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 - } - } - - StyledText { - visible: fontsSection.expanded - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Sans-serif font family") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledListView { - visible: fontsSection.expanded - Layout.fillWidth: true - implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - Layout.topMargin: 0 - Behavior on implicitHeight { - Anim {} + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + } } - model: Qt.fontFamilies() - spacing: Appearance.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Monospace font family") + font.pointSize: Appearance.font.size.larger + font.weight: 500 } - delegate: StyledRect { - required property string modelData + StyledListView { + Layout.fillWidth: true + implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - anchors.left: parent.left - anchors.right: parent.right + Behavior on implicitHeight { + Anim {} + } - readonly property bool isCurrent: modelData === root.fontFamilySans - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + model: Qt.fontFamilies() + spacing: Appearance.spacing.small / 2 + clip: true - StateLayer { - function onClicked(): void { - root.fontFamilySans = modelData; - root.saveConfig(); - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent } - RowLayout { - id: fontFamilySansRow + delegate: StyledRect { + required property string modelData anchors.left: parent.left anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal - - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } + readonly property bool isCurrent: modelData === root.fontFamilyMono + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - Loader { - active: isCurrent - asynchronous: true - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + StateLayer { + function onClicked(): void { + root.fontFamilyMono = modelData; + root.saveConfig(); } } - } - - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 - } - } - StyledRect { - visible: fontsSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: fontsSection.expanded ? fontSizeScaleRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + RowLayout { + id: fontFamilyMonoRow - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: fontSizeScaleRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Font size scale") - } - - CustomSpinBox { - id: fontSizeScaleSpinBox - min: 0.1 - max: 5 - value: root.fontSizeScale - onValueModified: value => { - root.fontSizeScale = value; - root.saveConfig(); - } - } - } - } - - Item { - id: scalesSection - Layout.fillWidth: true - Layout.preferredHeight: scalesSectionHeader.implicitHeight - property bool expanded: false + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - ColumnLayout { - id: scalesSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small + spacing: Appearance.spacing.normal - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Scales") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + Item { + Layout.fillWidth: true + } - Item { - Layout.fillWidth: true - } + Loader { + active: isCurrent + asynchronous: true - MaterialIcon { - text: "expand_more" - rotation: scalesSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = scalesSection.expanded; - root.collapseAllSections(scalesSection); - scalesSection.expanded = !wasExpanded; - } + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 } } - } - StyledRect { - visible: scalesSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: scalesSection.expanded ? paddingScaleRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Sans-serif font family") + font.pointSize: Appearance.font.size.larger + font.weight: 500 } - RowLayout { - id: paddingScaleRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Padding scale") - } + StyledListView { + Layout.fillWidth: true + implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - CustomSpinBox { - id: paddingScaleSpinBox - min: 0.1 - max: 5 - value: root.paddingScale - onValueModified: value => { - root.paddingScale = value; - root.saveConfig(); - } + Behavior on implicitHeight { + Anim {} } - } - } - StyledRect { - visible: scalesSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: scalesSection.expanded ? roundingScaleRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } + model: Qt.fontFamilies() + spacing: Appearance.spacing.small / 2 + clip: true - RowLayout { - id: roundingScaleRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Rounding scale") + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent } - CustomSpinBox { - id: roundingScaleSpinBox - min: 0.1 - max: 5 - value: root.roundingScale - onValueModified: value => { - root.roundingScale = value; - root.saveConfig(); - } - } - } - } + delegate: StyledRect { + required property string modelData - StyledRect { - visible: scalesSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: scalesSection.expanded ? spacingScaleRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } + anchors.left: parent.left + anchors.right: parent.right - RowLayout { - id: spacingScaleRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Spacing scale") - } + readonly property bool isCurrent: modelData === root.fontFamilySans + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - CustomSpinBox { - id: spacingScaleSpinBox - min: 0.1 - max: 5 - value: root.spacingScale - onValueModified: value => { - root.spacingScale = value; - root.saveConfig(); + StateLayer { + function onClicked(): void { + root.fontFamilySans = modelData; + root.saveConfig(); + } } - } - } - } - - Item { - id: transparencySection - Layout.fillWidth: true - Layout.preferredHeight: transparencySectionHeader.implicitHeight - property bool expanded: false - ColumnLayout { - id: transparencySectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small + RowLayout { + id: fontFamilySansRow - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Transparency") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + spacing: Appearance.spacing.normal - Item { - Layout.fillWidth: true - } - - MaterialIcon { - text: "expand_more" - rotation: transparencySection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal } - } - } - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = transparencySection.expanded; - root.collapseAllSections(transparencySection); - transparencySection.expanded = !wasExpanded; - } - } - } - } - - StyledRect { - visible: transparencySection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: transparencySection.expanded ? transparencyEnabledRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } + Item { + Layout.fillWidth: true + } - RowLayout { - id: transparencyEnabledRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Transparency enabled") - } + Loader { + active: isCurrent + asynchronous: true - StyledSwitch { - id: transparencyEnabledSwitch - checked: root.transparencyEnabled - onToggled: { - root.transparencyEnabled = checked; - root.saveConfig(); + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } } - } - } - } - - StyledRect { - visible: transparencySection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: transparencySection.expanded ? transparencyBaseRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: transparencyBaseRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Transparency base") + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 } - - CustomSpinBox { - id: transparencyBaseSpinBox - min: 0 - max: 1 - value: root.transparencyBase - onValueModified: value => { - root.transparencyBase = value; - root.saveConfig(); - } - } - } - } - - StyledRect { - visible: transparencySection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: transparencySection.expanded ? transparencyLayersRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} } - RowLayout { - id: transparencyLayersRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Transparency layers") - } + StyledRect { + Layout.fillWidth: true + implicitHeight: fontSizeScaleRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - CustomSpinBox { - id: transparencyLayersSpinBox - min: 0 - max: 1 - value: root.transparencyLayers - onValueModified: value => { - root.transparencyLayers = value; - root.saveConfig(); - } + Behavior on implicitHeight { + Anim {} } - } - } - - Item { - id: borderSection - Layout.fillWidth: true - Layout.preferredHeight: borderSectionHeader.implicitHeight - property bool expanded: false - - ColumnLayout { - id: borderSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + id: fontSizeScaleRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Border") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { Layout.fillWidth: true + text: qsTr("Font size scale") } - MaterialIcon { - text: "expand_more" - rotation: borderSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} + CustomSpinBox { + id: fontSizeScaleSpinBox + min: 0.1 + max: 5 + value: root.fontSizeScale + onValueModified: value => { + root.fontSizeScale = value; + root.saveConfig(); } } } - - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = borderSection.expanded; - root.collapseAllSections(borderSection); - borderSection.expanded = !wasExpanded; - } - } } } - StyledRect { - visible: borderSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: borderSection.expanded ? borderRoundingRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} + CollapsibleSection { + id: scalesSection + title: qsTr("Scales") + onToggleRequested: { + root.collapseAllSections(scalesSection); } - RowLayout { - id: borderRoundingRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Border rounding") + SpinBoxRow { + label: qsTr("Padding scale") + min: 0.1 + max: 5 + value: root.paddingScale + onValueModified: value => { + root.paddingScale = value; + root.saveConfig(); } - - CustomSpinBox { - id: borderRoundingSpinBox - min: 0.1 - max: 5 - value: root.borderRounding - onValueModified: value => { - root.borderRounding = value; - root.saveConfig(); - } - } - } - } - - StyledRect { - visible: borderSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: borderSection.expanded ? borderThicknessRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} } - RowLayout { - id: borderThicknessRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Border thickness") - } - - CustomSpinBox { - id: borderThicknessSpinBox - min: 0.1 - max: 5 - value: root.borderThickness - onValueModified: value => { - root.borderThickness = value; - root.saveConfig(); - } + SpinBoxRow { + label: qsTr("Rounding scale") + min: 0.1 + max: 5 + value: root.roundingScale + onValueModified: value => { + root.roundingScale = value; + root.saveConfig(); } } - } - - Item { - id: backgroundSection - Layout.fillWidth: true - Layout.preferredHeight: backgroundSectionHeader.implicitHeight - property bool expanded: false - - ColumnLayout { - id: backgroundSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Background") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - MaterialIcon { - text: "expand_more" - rotation: backgroundSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} - } - } - } - - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = backgroundSection.expanded; - root.collapseAllSections(backgroundSection); - backgroundSection.expanded = !wasExpanded; - } + SpinBoxRow { + label: qsTr("Spacing scale") + min: 0.1 + max: 5 + value: root.spacingScale + onValueModified: value => { + root.spacingScale = value; + root.saveConfig(); } } } - StyledRect { - visible: backgroundSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: backgroundSection.expanded ? desktopClockRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} + CollapsibleSection { + id: transparencySection + title: qsTr("Transparency") + onToggleRequested: { + root.collapseAllSections(transparencySection); } - RowLayout { - id: desktopClockRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Desktop clock") + SwitchRow { + label: qsTr("Transparency enabled") + checked: root.transparencyEnabled + onToggled: checked => { + root.transparencyEnabled = checked; + root.saveConfig(); } - - StyledSwitch { - id: desktopClockSwitch - checked: root.desktopClockEnabled - onToggled: { - root.desktopClockEnabled = checked; - root.saveConfig(); - } - } - } - } - - StyledRect { - visible: backgroundSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: backgroundSection.expanded ? backgroundEnabledRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} } - RowLayout { - id: backgroundEnabledRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Background enabled") + SpinBoxRow { + label: qsTr("Transparency base") + min: 0 + max: 1 + value: root.transparencyBase + onValueModified: value => { + root.transparencyBase = value; + root.saveConfig(); } + } - StyledSwitch { - id: backgroundEnabledSwitch - checked: root.backgroundEnabled - onToggled: { - root.backgroundEnabled = checked; - root.saveConfig(); - } + SpinBoxRow { + label: qsTr("Transparency layers") + min: 0 + max: 1 + value: root.transparencyLayers + onValueModified: value => { + root.transparencyLayers = value; + root.saveConfig(); } } } - StyledText { - visible: backgroundSection.expanded - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Visualiser") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledRect { - visible: backgroundSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: backgroundSection.expanded ? visualiserEnabledRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} + CollapsibleSection { + id: borderSection + title: qsTr("Border") + onToggleRequested: { + root.collapseAllSections(borderSection); } - RowLayout { - id: visualiserEnabledRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Visualiser enabled") + SpinBoxRow { + label: qsTr("Border rounding") + min: 0.1 + max: 5 + value: root.borderRounding + onValueModified: value => { + root.borderRounding = value; + root.saveConfig(); } + } - StyledSwitch { - id: visualiserEnabledSwitch - checked: root.visualiserEnabled - onToggled: { - root.visualiserEnabled = checked; - root.saveConfig(); - } + SpinBoxRow { + label: qsTr("Border thickness") + min: 0.1 + max: 5 + value: root.borderThickness + onValueModified: value => { + root.borderThickness = value; + root.saveConfig(); } } } - StyledRect { - visible: backgroundSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: backgroundSection.expanded ? visualiserAutoHideRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} + CollapsibleSection { + id: backgroundSection + title: qsTr("Background") + onToggleRequested: { + root.collapseAllSections(backgroundSection); } - RowLayout { - id: visualiserAutoHideRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Visualiser auto hide") + SwitchRow { + label: qsTr("Desktop clock") + checked: root.desktopClockEnabled + onToggled: checked => { + root.desktopClockEnabled = checked; + root.saveConfig(); } + } - StyledSwitch { - id: visualiserAutoHideSwitch - checked: root.visualiserAutoHide - onToggled: { - root.visualiserAutoHide = checked; - root.saveConfig(); - } + SwitchRow { + label: qsTr("Background enabled") + checked: root.backgroundEnabled + onToggled: checked => { + root.backgroundEnabled = checked; + root.saveConfig(); } } - } - StyledRect { - visible: backgroundSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: backgroundSection.expanded ? visualiserRoundingRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Visualiser") + font.pointSize: Appearance.font.size.larger + font.weight: 500 } - RowLayout { - id: visualiserRoundingRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Visualiser rounding") - } - - CustomSpinBox { - id: visualiserRoundingSpinBox - min: 0 - max: 10 - value: Math.round(root.visualiserRounding) - onValueModified: value => { - root.visualiserRounding = value; - root.saveConfig(); - } + SwitchRow { + label: qsTr("Visualiser enabled") + checked: root.visualiserEnabled + onToggled: checked => { + root.visualiserEnabled = checked; + root.saveConfig(); } } - } - - StyledRect { - visible: backgroundSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: backgroundSection.expanded ? visualiserSpacingRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - Behavior on implicitHeight { - Anim {} + SwitchRow { + label: qsTr("Visualiser auto hide") + checked: root.visualiserAutoHide + onToggled: checked => { + root.visualiserAutoHide = checked; + root.saveConfig(); + } } - RowLayout { - id: visualiserSpacingRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Visualiser spacing") + SpinBoxRow { + label: qsTr("Visualiser rounding") + min: 0 + max: 10 + value: Math.round(root.visualiserRounding) + onValueModified: value => { + root.visualiserRounding = value; + root.saveConfig(); } + } - CustomSpinBox { - id: visualiserSpacingSpinBox - min: 0 - max: 10 - value: Math.round(root.visualiserSpacing) - onValueModified: value => { - root.visualiserSpacing = value; - root.saveConfig(); - } + SpinBoxRow { + label: qsTr("Visualiser spacing") + min: 0 + max: 10 + value: Math.round(root.visualiserSpacing) + onValueModified: value => { + root.visualiserSpacing = value; + root.saveConfig(); } } } diff --git a/modules/controlcenter/ethernet/EthernetList.qml b/modules/controlcenter/ethernet/EthernetList.qml index ff7cee2..8b04c09 100644 --- a/modules/controlcenter/ethernet/EthernetList.qml +++ b/modules/controlcenter/ethernet/EthernetList.qml @@ -34,7 +34,7 @@ ColumnLayout { icon: "settings" accent: "Primary" - function onClicked(): void { + onClicked: { if (root.session.ethernet.active) root.session.ethernet.active = null; else { @@ -166,81 +166,6 @@ ColumnLayout { implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } - - component ToggleButton: StyledRect { - id: toggleBtn - - required property bool toggled - property string icon - property string label - property string accent: "Secondary" - - function onClicked(): void { - } - - Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) - implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2 - implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2 - - radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) - color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] - - StateLayer { - id: toggleStateLayer - - color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] - - function onClicked(): void { - toggleBtn.onClicked(); - } - } - - RowLayout { - id: toggleBtnInner - - anchors.centerIn: parent - spacing: Appearance.spacing.normal - - MaterialIcon { - id: toggleBtnIcon - - visible: !!text - fill: toggleBtn.toggled ? 1 : 0 - text: toggleBtn.icon - color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] - font.pointSize: Appearance.font.size.large - - Behavior on fill { - Anim {} - } - } - - Loader { - asynchronous: true - active: !!toggleBtn.label - visible: active - - sourceComponent: StyledText { - text: toggleBtn.label - color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] - } - } - } - - Behavior on radius { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } - - Behavior on Layout.preferredWidth { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } - } } diff --git a/modules/controlcenter/network/NetworkList.qml b/modules/controlcenter/network/NetworkList.qml index 60bd857..09d7352 100644 --- a/modules/controlcenter/network/NetworkList.qml +++ b/modules/controlcenter/network/NetworkList.qml @@ -34,7 +34,7 @@ ColumnLayout { icon: "wifi" accent: "Tertiary" - function onClicked(): void { + onClicked: { Network.toggleWifi(); } } @@ -44,7 +44,7 @@ ColumnLayout { icon: "wifi_find" accent: "Secondary" - function onClicked(): void { + onClicked: { Network.rescanWifi(); } } @@ -54,7 +54,7 @@ ColumnLayout { icon: "settings" accent: "Primary" - function onClicked(): void { + onClicked: { if (root.session.network.active) root.session.network.active = null; else { @@ -224,79 +224,4 @@ ColumnLayout { implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } - - component ToggleButton: StyledRect { - id: toggleBtn - - required property bool toggled - property string icon - property string label - property string accent: "Secondary" - - function onClicked(): void { - } - - Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) - implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2 - implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2 - - radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) - color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] - - StateLayer { - id: toggleStateLayer - - color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] - - function onClicked(): void { - toggleBtn.onClicked(); - } - } - - RowLayout { - id: toggleBtnInner - - anchors.centerIn: parent - spacing: Appearance.spacing.normal - - MaterialIcon { - id: toggleBtnIcon - - visible: !!text - fill: toggleBtn.toggled ? 1 : 0 - text: toggleBtn.icon - color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] - font.pointSize: Appearance.font.size.large - - Behavior on fill { - Anim {} - } - } - - Loader { - asynchronous: true - active: !!toggleBtn.label - visible: active - - sourceComponent: StyledText { - text: toggleBtn.label - color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] - } - } - } - - Behavior on radius { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } - - Behavior on Layout.preferredWidth { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } - } } diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 22414c7..2bb50d8 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -239,73 +239,12 @@ RowLayout { } } - Item { + CollapsibleSection { id: clockSection - Layout.fillWidth: true - Layout.preferredHeight: clockSectionHeader.implicitHeight - property bool expanded: false - - ColumnLayout { - id: clockSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Clock") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - MaterialIcon { - text: "expand_more" - rotation: clockSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} - } - } - } - - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = clockSection.expanded; - root.collapseAllSections(clockSection); - clockSection.expanded = !wasExpanded; - } - } - - StyledText { - visible: clockSection.expanded - text: qsTr("Clock display settings") - color: Colours.palette.m3outline - Layout.fillWidth: true - } - } - } - - StyledRect { - Layout.fillWidth: true - visible: clockSection.expanded - implicitHeight: clockSection.expanded ? clockRow.implicitHeight + Appearance.padding.large * 2 : 0 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} + title: qsTr("Clock") + description: qsTr("Clock display settings") + onToggleRequested: { + root.collapseAllSections(clockSection); } RowLayout { @@ -333,835 +272,319 @@ RowLayout { } } - Item { + CollapsibleSection { id: barBehaviorSection - Layout.fillWidth: true - Layout.preferredHeight: barBehaviorSectionHeader.implicitHeight - property bool expanded: false - - ColumnLayout { - id: barBehaviorSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Bar Behavior") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - MaterialIcon { - text: "expand_more" - rotation: barBehaviorSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} - } - } - } - - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = barBehaviorSection.expanded; - root.collapseAllSections(barBehaviorSection); - barBehaviorSection.expanded = !wasExpanded; - } - } + title: qsTr("Bar Behavior") + onToggleRequested: { + root.collapseAllSections(barBehaviorSection); } - } - - StyledRect { - visible: barBehaviorSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: barBehaviorSection.expanded ? persistentRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: persistentRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Persistent") - } - StyledSwitch { - checked: root.persistent - onToggled: { - root.persistent = checked; - root.saveConfig(); - } + SwitchRow { + label: qsTr("Persistent") + checked: root.persistent + onToggled: checked => { + root.persistent = checked; + root.saveConfig(); } } - } - - StyledRect { - visible: barBehaviorSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: barBehaviorSection.expanded ? showOnHoverRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: showOnHoverRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show on hover") - } - StyledSwitch { - checked: root.showOnHover - onToggled: { - root.showOnHover = checked; - root.saveConfig(); - } + SwitchRow { + label: qsTr("Show on hover") + checked: root.showOnHover + onToggled: checked => { + root.showOnHover = checked; + root.saveConfig(); } } - } - - StyledRect { - visible: barBehaviorSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: barBehaviorSection.expanded ? dragThresholdRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - RowLayout { - id: dragThresholdRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Drag threshold") - } - - CustomSpinBox { - min: 0 - max: 100 - value: root.dragThreshold - onValueModified: value => { - root.dragThreshold = value; - root.saveConfig(); - } + SpinBoxRow { + label: qsTr("Drag threshold") + min: 0 + max: 100 + value: root.dragThreshold + onValueModified: value => { + root.dragThreshold = value; + root.saveConfig(); } } } - Item { + CollapsibleSection { id: statusIconsSection - Layout.fillWidth: true - Layout.preferredHeight: statusIconsSectionHeader.implicitHeight - property bool expanded: false - - ColumnLayout { - id: statusIconsSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Status Icons") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - MaterialIcon { - text: "expand_more" - rotation: statusIconsSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} - } - } - } - - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = statusIconsSection.expanded; - root.collapseAllSections(statusIconsSection); - statusIconsSection.expanded = !wasExpanded; - } - } + title: qsTr("Status Icons") + onToggleRequested: { + root.collapseAllSections(statusIconsSection); } - } - - StyledRect { - visible: statusIconsSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: statusIconsSection.expanded ? showAudioRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: showAudioRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show audio") - } - StyledSwitch { - checked: root.showAudio - onToggled: { - root.showAudio = checked; - root.saveConfig(); - } + SwitchRow { + label: qsTr("Show audio") + checked: root.showAudio + onToggled: checked => { + root.showAudio = checked; + root.saveConfig(); } } - } - - StyledRect { - visible: statusIconsSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: statusIconsSection.expanded ? showMicrophoneRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - RowLayout { - id: showMicrophoneRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show microphone") + SwitchRow { + label: qsTr("Show microphone") + checked: root.showMicrophone + onToggled: checked => { + root.showMicrophone = checked; + root.saveConfig(); } - - StyledSwitch { - checked: root.showMicrophone - onToggled: { - root.showMicrophone = checked; - root.saveConfig(); - } - } - } - } - - StyledRect { - visible: statusIconsSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: statusIconsSection.expanded ? showKbLayoutRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} } - RowLayout { - id: showKbLayoutRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show keyboard layout") + SwitchRow { + label: qsTr("Show keyboard layout") + checked: root.showKbLayout + onToggled: checked => { + root.showKbLayout = checked; + root.saveConfig(); } + } - StyledSwitch { - checked: root.showKbLayout - onToggled: { - root.showKbLayout = checked; - root.saveConfig(); - } + SwitchRow { + label: qsTr("Show network") + checked: root.showNetwork + onToggled: checked => { + root.showNetwork = checked; + root.saveConfig(); } } - } - StyledRect { - visible: statusIconsSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: statusIconsSection.expanded ? showNetworkRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} + SwitchRow { + label: qsTr("Show bluetooth") + checked: root.showBluetooth + onToggled: checked => { + root.showBluetooth = checked; + root.saveConfig(); + } } - RowLayout { - id: showNetworkRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show network") + SwitchRow { + label: qsTr("Show battery") + checked: root.showBattery + onToggled: checked => { + root.showBattery = checked; + root.saveConfig(); } + } - StyledSwitch { - checked: root.showNetwork - onToggled: { - root.showNetwork = checked; - root.saveConfig(); - } + SwitchRow { + label: qsTr("Show lock status") + checked: root.showLockStatus + onToggled: checked => { + root.showLockStatus = checked; + root.saveConfig(); } } } - StyledRect { - visible: statusIconsSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: statusIconsSection.expanded ? showBluetoothRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} + CollapsibleSection { + id: traySettingsSection + title: qsTr("Tray Settings") + onToggleRequested: { + root.collapseAllSections(traySettingsSection); } - RowLayout { - id: showBluetoothRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show bluetooth") + SwitchRow { + label: qsTr("Background") + checked: root.trayBackground + onToggled: checked => { + root.trayBackground = checked; + root.saveConfig(); } - - StyledSwitch { - checked: root.showBluetooth - onToggled: { - root.showBluetooth = checked; - root.saveConfig(); - } - } - } - } - - StyledRect { - visible: statusIconsSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: statusIconsSection.expanded ? showBatteryRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} } - RowLayout { - id: showBatteryRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show battery") + SwitchRow { + label: qsTr("Compact") + checked: root.trayCompact + onToggled: checked => { + root.trayCompact = checked; + root.saveConfig(); } + } - StyledSwitch { - checked: root.showBattery - onToggled: { - root.showBattery = checked; - root.saveConfig(); - } + SwitchRow { + label: qsTr("Recolour") + checked: root.trayRecolour + onToggled: checked => { + root.trayRecolour = checked; + root.saveConfig(); } } } - StyledRect { - visible: statusIconsSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: statusIconsSection.expanded ? showLockStatusRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} + CollapsibleSection { + id: workspacesSection + title: qsTr("Workspaces") + onToggleRequested: { + root.collapseAllSections(workspacesSection); } - RowLayout { - id: showLockStatusRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show lock status") - } + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - StyledSwitch { - checked: root.showLockStatus - onToggled: { - root.showLockStatus = checked; - root.saveConfig(); - } + Behavior on implicitHeight { + Anim {} } - } - } - - Item { - id: traySettingsSection - Layout.fillWidth: true - Layout.preferredHeight: traySettingsSectionHeader.implicitHeight - property bool expanded: false - - ColumnLayout { - id: traySettingsSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + id: workspacesShownRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Tray Settings") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { Layout.fillWidth: true + text: qsTr("Shown") } - MaterialIcon { - text: "expand_more" - rotation: traySettingsSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} + CustomSpinBox { + min: 1 + max: 20 + value: root.workspacesShown + onValueModified: value => { + root.workspacesShown = value; + root.saveConfig(); } } } - - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = traySettingsSection.expanded; - root.collapseAllSections(traySettingsSection); - traySettingsSection.expanded = !wasExpanded; - } - } } - } - - StyledRect { - visible: traySettingsSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: traySettingsSection.expanded ? trayBackgroundRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: trayBackgroundRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - StyledText { - Layout.fillWidth: true - text: qsTr("Background") - } + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - StyledSwitch { - checked: root.trayBackground - onToggled: { - root.trayBackground = checked; - root.saveConfig(); - } + Behavior on implicitHeight { + Anim {} } - } - } - - StyledRect { - visible: traySettingsSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: traySettingsSection.expanded ? trayCompactRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: trayCompactRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Compact") - } + RowLayout { + id: workspacesActiveIndicatorRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - StyledSwitch { - checked: root.trayCompact - onToggled: { - root.trayCompact = checked; - root.saveConfig(); + StyledText { + Layout.fillWidth: true + text: qsTr("Active indicator") } - } - } - } - - StyledRect { - visible: traySettingsSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: traySettingsSection.expanded ? trayRecolourRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - RowLayout { - id: trayRecolourRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Recolour") - } - - StyledSwitch { - checked: root.trayRecolour - onToggled: { - root.trayRecolour = checked; - root.saveConfig(); + StyledSwitch { + checked: root.workspacesActiveIndicator + onToggled: { + root.workspacesActiveIndicator = checked; + root.saveConfig(); + } } } } - } - Item { - id: workspacesSection - Layout.fillWidth: true - Layout.preferredHeight: workspacesSectionHeader.implicitHeight - property bool expanded: false + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - ColumnLayout { - id: workspacesSectionHeader - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small + Behavior on implicitHeight { + Anim {} + } RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + id: workspacesOccupiedBgRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Workspaces") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { Layout.fillWidth: true + text: qsTr("Occupied background") } - MaterialIcon { - text: "expand_more" - rotation: workspacesSection.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} + StyledSwitch { + checked: root.workspacesOccupiedBg + onToggled: { + root.workspacesOccupiedBg = checked; + root.saveConfig(); } } } - - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - const wasExpanded = workspacesSection.expanded; - root.collapseAllSections(workspacesSection); - workspacesSection.expanded = !wasExpanded; - } - } } - } - StyledRect { - visible: workspacesSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: workspacesSection.expanded ? workspacesShownRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesShownRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Shown") - } + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - CustomSpinBox { - min: 1 - max: 20 - value: root.workspacesShown - onValueModified: value => { - root.workspacesShown = value; - root.saveConfig(); - } + Behavior on implicitHeight { + Anim {} } - } - } - StyledRect { - visible: workspacesSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: workspacesSection.expanded ? workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesActiveIndicatorRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Active indicator") - } + RowLayout { + id: workspacesShowWindowsRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - StyledSwitch { - checked: root.workspacesActiveIndicator - onToggled: { - root.workspacesActiveIndicator = checked; - root.saveConfig(); + StyledText { + Layout.fillWidth: true + text: qsTr("Show windows") } - } - } - } - - StyledRect { - visible: workspacesSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: workspacesSection.expanded ? workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesOccupiedBgRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Occupied background") - } - StyledSwitch { - checked: root.workspacesOccupiedBg - onToggled: { - root.workspacesOccupiedBg = checked; - root.saveConfig(); + StyledSwitch { + checked: root.workspacesShowWindows + onToggled: { + root.workspacesShowWindows = checked; + root.saveConfig(); + } } } } - } - StyledRect { - visible: workspacesSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: workspacesSection.expanded ? workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesShowWindowsRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show windows") - } + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - StyledSwitch { - checked: root.workspacesShowWindows - onToggled: { - root.workspacesShowWindows = checked; - root.saveConfig(); - } + Behavior on implicitHeight { + Anim {} } - } - } - StyledRect { - visible: workspacesSection.expanded - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.small / 2 - implicitHeight: workspacesSection.expanded ? workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 : 0 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesPerMonitorRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + RowLayout { + id: workspacesPerMonitorRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - StyledText { - Layout.fillWidth: true - text: qsTr("Per monitor workspaces") - } + StyledText { + Layout.fillWidth: true + text: qsTr("Per monitor workspaces") + } - StyledSwitch { - checked: root.workspacesPerMonitor - onToggled: { - root.workspacesPerMonitor = checked; - root.saveConfig(); + StyledSwitch { + checked: root.workspacesPerMonitor + onToggled: { + root.workspacesPerMonitor = checked; + root.saveConfig(); + } } } } -- cgit v1.2.3-freya From e21a1519b0b59da0847f8c215201720fef3d854e Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 16:06:16 -0500 Subject: controlcenter: refactoring into components --- components/controls/SwitchRow.qml | 2 + .../controlcenter/appearance/AppearancePane.qml | 78 ++++------------------ modules/controlcenter/launcher/LauncherPane.qml | 45 ++++--------- 3 files changed, 27 insertions(+), 98 deletions(-) diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml index a486ee2..0ec7aa5 100644 --- a/components/controls/SwitchRow.qml +++ b/components/controls/SwitchRow.qml @@ -11,6 +11,7 @@ StyledRect { required property string label required property bool checked + property bool enabled: true property var onToggled: function(checked) {} Layout.fillWidth: true @@ -38,6 +39,7 @@ StyledRect { StyledSwitch { checked: root.checked + enabled: root.enabled onToggled: { root.onToggled(checked); } diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index cfe5b56..68e2e2d 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -263,38 +263,11 @@ RowLayout { root.collapseAllSections(themeModeSection); } - StyledRect { - Layout.fillWidth: true - implicitHeight: modeToggle.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: modeToggle - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Dark mode") - } - - StyledSwitch { - checked: !Colours.currentLight - onToggled: { - Colours.setMode(checked ? "dark" : "light"); - } - } + SwitchRow { + label: qsTr("Dark mode") + checked: !Colours.currentLight + onToggled: checked => { + Colours.setMode(checked ? "dark" : "light"); } } } @@ -803,39 +776,14 @@ RowLayout { } } - StyledRect { - Layout.fillWidth: true - implicitHeight: fontSizeScaleRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: fontSizeScaleRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Font size scale") - } - - CustomSpinBox { - id: fontSizeScaleSpinBox - min: 0.1 - max: 5 - value: root.fontSizeScale - onValueModified: value => { - root.fontSizeScale = value; - root.saveConfig(); - } - } + SpinBoxRow { + label: qsTr("Font size scale") + min: 0.1 + max: 5 + value: root.fontSizeScale + onValueModified: value => { + root.fontSizeScale = value; + root.saveConfig(); } } } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 82e145a..9b2570a 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -22,6 +22,7 @@ RowLayout { required property Session session property var selectedApp: null + property bool hideFromLauncherChecked: false anchors.fill: parent @@ -45,7 +46,7 @@ RowLayout { function updateToggleState() { if (!root.selectedApp || !configFile.loaded) { - hideFromLauncherSwitch.checked = false; + root.hideFromLauncherChecked = false; return; } @@ -54,16 +55,16 @@ RowLayout { const appId = root.selectedApp.id || root.selectedApp.entry?.id; if (config.launcher && config.launcher.hiddenApps) { - hideFromLauncherSwitch.checked = config.launcher.hiddenApps.includes(appId); + root.hideFromLauncherChecked = config.launcher.hiddenApps.includes(appId); } else { - hideFromLauncherSwitch.checked = false; + root.hideFromLauncherChecked = false; } } catch (e) { console.error("Failed to update toggle state:", e); } } - function saveHiddenApps() { + function saveHiddenApps(isHidden) { if (!configFile.loaded || !root.selectedApp) { return; } @@ -76,7 +77,6 @@ RowLayout { if (!config.launcher.hiddenApps) config.launcher.hiddenApps = []; const hiddenApps = config.launcher.hiddenApps; - const isHidden = hideFromLauncherSwitch.checked; if (isHidden) { // Add to hiddenApps if not already there @@ -288,35 +288,14 @@ RowLayout { anchors.top: parent.top spacing: Appearance.spacing.normal - StyledRect { - Layout.fillWidth: true + SwitchRow { Layout.topMargin: Appearance.spacing.normal - implicitHeight: hideToggleRow.implicitHeight + Appearance.padding.large * 2 - color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal - - RowLayout { - id: hideToggleRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Hide from launcher") - font.pointSize: Appearance.font.size.normal - } - - StyledSwitch { - id: hideFromLauncherSwitch - checked: false - enabled: root.selectedApp !== null && configFile.loaded - onToggled: { - root.saveHiddenApps(); - } - } + label: qsTr("Hide from launcher") + checked: root.hideFromLauncherChecked + enabled: root.selectedApp !== null && configFile.loaded + onToggled: checked => { + root.hideFromLauncherChecked = checked; + root.saveHiddenApps(checked); } } -- cgit v1.2.3-freya From 893a91a95a11cb57ffe2fbfa0ec221c1a309528b Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 16:36:42 -0500 Subject: controlcenter: refactored wifi/ethernet panels --- components/ConnectionHeader.qml | 32 +++ components/ConnectionInfoSection.qml | 60 ++++ components/PropertyRow.qml | 27 ++ components/SectionContainer.qml | 31 +++ components/SectionHeader.qml | 28 ++ components/controls/ToggleRow.qml | 29 ++ modules/controlcenter/ethernet/EthernetDetails.qml | 238 +++------------- modules/controlcenter/network/Details.qml | 305 +++++---------------- plan.plan.md | 113 ++++++++ 9 files changed, 432 insertions(+), 431 deletions(-) create mode 100644 components/ConnectionHeader.qml create mode 100644 components/ConnectionInfoSection.qml create mode 100644 components/PropertyRow.qml create mode 100644 components/SectionContainer.qml create mode 100644 components/SectionHeader.qml create mode 100644 components/controls/ToggleRow.qml create mode 100644 plan.plan.md diff --git a/components/ConnectionHeader.qml b/components/ConnectionHeader.qml new file mode 100644 index 0000000..3f77fd9 --- /dev/null +++ b/components/ConnectionHeader.qml @@ -0,0 +1,32 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property string icon + required property string title + + spacing: Appearance.spacing.normal + Layout.alignment: Qt.AlignHCenter + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + animate: true + text: root.icon + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + animate: true + text: root.title + font.pointSize: Appearance.font.size.large + font.bold: true + } +} + diff --git a/components/ConnectionInfoSection.qml b/components/ConnectionInfoSection.qml new file mode 100644 index 0000000..88c6b3a --- /dev/null +++ b/components/ConnectionInfoSection.qml @@ -0,0 +1,60 @@ +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property var deviceDetails + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("IP Address") + } + + StyledText { + text: root.deviceDetails?.ipAddress || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Subnet Mask") + } + + StyledText { + text: root.deviceDetails?.subnet || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Gateway") + } + + StyledText { + text: root.deviceDetails?.gateway || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("DNS Servers") + } + + StyledText { + text: (root.deviceDetails && root.deviceDetails.dns && root.deviceDetails.dns.length > 0) ? root.deviceDetails.dns.join(", ") : qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + wrapMode: Text.Wrap + Layout.maximumWidth: parent.width + } +} + diff --git a/components/PropertyRow.qml b/components/PropertyRow.qml new file mode 100644 index 0000000..697830a --- /dev/null +++ b/components/PropertyRow.qml @@ -0,0 +1,27 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property string label + required property string value + property bool showTopMargin: false + + spacing: Appearance.spacing.small / 2 + + StyledText { + Layout.topMargin: root.showTopMargin ? Appearance.spacing.normal : 0 + text: root.label + } + + StyledText { + text: root.value + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } +} + diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml new file mode 100644 index 0000000..d41254b --- /dev/null +++ b/components/SectionContainer.qml @@ -0,0 +1,31 @@ +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + default property alias content: contentColumn.data + property real contentSpacing: Appearance.spacing.larger + + Layout.fillWidth: true + implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: contentColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: root.contentSpacing + } +} + diff --git a/components/SectionHeader.qml b/components/SectionHeader.qml new file mode 100644 index 0000000..897e63a --- /dev/null +++ b/components/SectionHeader.qml @@ -0,0 +1,28 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property string title + property string description: "" + + spacing: 0 + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: root.title + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + visible: root.description !== "" + text: root.description + color: Colours.palette.m3outline + } +} + diff --git a/components/controls/ToggleRow.qml b/components/controls/ToggleRow.qml new file mode 100644 index 0000000..23dc2a2 --- /dev/null +++ b/components/controls/ToggleRow.qml @@ -0,0 +1,29 @@ +import qs.components +import qs.components.controls +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property string label + property alias checked: toggle.checked + property alias toggle: toggle + + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + } + + StyledSwitch { + id: toggle + + cLayer: 2 + } +} + diff --git a/modules/controlcenter/ethernet/EthernetDetails.qml b/modules/controlcenter/ethernet/EthernetDetails.qml index 1db3db0..a49eb4f 100644 --- a/modules/controlcenter/ethernet/EthernetDetails.qml +++ b/modules/controlcenter/ethernet/EthernetDetails.qml @@ -43,229 +43,73 @@ Item { anchors.right: parent.right spacing: Appearance.spacing.normal - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - animate: true - text: "cable" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true + ConnectionHeader { + icon: "cable" + title: root.device?.interface ?? qsTr("Unknown") } - StyledText { - Layout.alignment: Qt.AlignHCenter - animate: true - text: root.device?.interface ?? qsTr("Unknown") - font.pointSize: Appearance.font.size.large - font.bold: true + SectionHeader { + title: qsTr("Connection status") + description: qsTr("Connection settings for this device") } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Connection status") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("Connection settings for this device") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: deviceStatus - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.larger - - Toggle { - label: qsTr("Connected") - checked: root.device?.connected ?? false - toggle.onToggled: { - if (checked) { - // Use connection name if available, otherwise use interface - Network.connectEthernet(root.device?.connection || "", root.device?.interface || ""); - } else { - if (root.device?.connection) { - Network.disconnectEthernet(root.device.connection); - } + SectionContainer { + ToggleRow { + label: qsTr("Connected") + checked: root.device?.connected ?? false + toggle.onToggled: { + if (checked) { + // Use connection name if available, otherwise use interface + Network.connectEthernet(root.device?.connection || "", root.device?.interface || ""); + } else { + if (root.device?.connection) { + Network.disconnectEthernet(root.device.connection); } } } } } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Device properties") - font.pointSize: Appearance.font.size.larger - font.weight: 500 + SectionHeader { + title: qsTr("Device properties") + description: qsTr("Additional information") } - StyledText { - text: qsTr("Additional information") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 - ColumnLayout { - id: deviceProps - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.small / 2 - - StyledText { - text: qsTr("Interface") - } - - StyledText { - text: root.device?.interface ?? qsTr("Unknown") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Connection") - } - - StyledText { - text: root.device?.connection || qsTr("Not connected") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("State") - } + PropertyRow { + label: qsTr("Interface") + value: root.device?.interface ?? qsTr("Unknown") + } - StyledText { - text: root.device?.state ?? qsTr("Unknown") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } + PropertyRow { + showTopMargin: true + label: qsTr("Connection") + value: root.device?.connection || qsTr("Not connected") } - } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Connection information") - font.pointSize: Appearance.font.size.larger - font.weight: 500 + PropertyRow { + showTopMargin: true + label: qsTr("State") + value: root.device?.state ?? qsTr("Unknown") + } } - StyledText { - text: qsTr("Network connection details") - color: Colours.palette.m3outline + SectionHeader { + title: qsTr("Connection information") + description: qsTr("Network connection details") } - StyledRect { - Layout.fillWidth: true - implicitHeight: connectionInfo.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: connectionInfo - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.small / 2 - - StyledText { - text: qsTr("IP Address") - } - - StyledText { - text: Network.ethernetDeviceDetails?.ipAddress || qsTr("Not available") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Subnet Mask") - } - - StyledText { - text: Network.ethernetDeviceDetails?.subnet || qsTr("Not available") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Gateway") - } - - StyledText { - text: Network.ethernetDeviceDetails?.gateway || qsTr("Not available") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("DNS Servers") - } - - StyledText { - text: (Network.ethernetDeviceDetails && Network.ethernetDeviceDetails.dns && Network.ethernetDeviceDetails.dns.length > 0) ? Network.ethernetDeviceDetails.dns.join(", ") : qsTr("Not available") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - wrapMode: Text.Wrap - Layout.maximumWidth: parent.width - } + SectionContainer { + ConnectionInfoSection { + deviceDetails: Network.ethernetDeviceDetails } } } } - component Toggle: RowLayout { - required property string label - property alias checked: toggle.checked - property alias toggle: toggle - - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: parent.label - } - - StyledSwitch { - id: toggle - - cLayer: 2 - } - } } diff --git a/modules/controlcenter/network/Details.qml b/modules/controlcenter/network/Details.qml index 31d20bc..a53f62e 100644 --- a/modules/controlcenter/network/Details.qml +++ b/modules/controlcenter/network/Details.qml @@ -54,272 +54,109 @@ Item { anchors.right: parent.right spacing: Appearance.spacing.normal - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - animate: true - text: root.network?.isSecure ? "lock" : "wifi" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true + ConnectionHeader { + icon: root.network?.isSecure ? "lock" : "wifi" + title: root.network?.ssid ?? qsTr("Unknown") } - StyledText { - Layout.alignment: Qt.AlignHCenter - animate: true - text: root.network?.ssid ?? qsTr("Unknown") - font.pointSize: Appearance.font.size.large - font.bold: true + SectionHeader { + title: qsTr("Connection status") + description: qsTr("Connection settings for this network") } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Connection status") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("Connection settings for this network") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: networkStatus.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: networkStatus - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.larger - - Toggle { - label: qsTr("Connected") - checked: root.network?.active ?? false - toggle.onToggled: { - if (checked) { - // If already connected to a different network, disconnect first - if (Network.active && Network.active.ssid !== root.network.ssid) { - Network.disconnectFromNetwork(); - // Wait a moment before connecting to new network - Qt.callLater(() => { - connectToNetwork(); - }); - } else { + SectionContainer { + ToggleRow { + label: qsTr("Connected") + checked: root.network?.active ?? false + toggle.onToggled: { + if (checked) { + // If already connected to a different network, disconnect first + if (Network.active && Network.active.ssid !== root.network.ssid) { + Network.disconnectFromNetwork(); + // Wait a moment before connecting to new network + Qt.callLater(() => { connectToNetwork(); - } + }); } else { - Network.disconnectFromNetwork(); + connectToNetwork(); } + } else { + Network.disconnectFromNetwork(); } + } - function connectToNetwork(): void { - if (root.network.isSecure) { - // Try connecting without password first (in case it's saved) - Network.connectToNetworkWithPasswordCheck( - root.network.ssid, - root.network.isSecure, - () => { - // Callback: connection failed, show password dialog - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = root.network; - } - ); - } else { - Network.connectToNetwork(root.network.ssid, ""); - } + function connectToNetwork(): void { + if (root.network.isSecure) { + // Try connecting without password first (in case it's saved) + Network.connectToNetworkWithPasswordCheck( + root.network.ssid, + root.network.isSecure, + () => { + // Callback: connection failed, show password dialog + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = root.network; + } + ); + } else { + Network.connectToNetwork(root.network.ssid, ""); } } } } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Network properties") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("Additional information") - color: Colours.palette.m3outline + SectionHeader { + title: qsTr("Network properties") + description: qsTr("Additional information") } - StyledRect { - Layout.fillWidth: true - implicitHeight: networkProps.implicitHeight + Appearance.padding.large * 2 + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: networkProps - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.small / 2 - - StyledText { - text: qsTr("SSID") - } - - StyledText { - text: root.network?.ssid ?? qsTr("Unknown") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("BSSID") - } - - StyledText { - text: root.network?.bssid ?? qsTr("Unknown") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Signal strength") - } - - StyledText { - text: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Frequency") - } + PropertyRow { + label: qsTr("SSID") + value: root.network?.ssid ?? qsTr("Unknown") + } - StyledText { - text: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } + PropertyRow { + showTopMargin: true + label: qsTr("BSSID") + value: root.network?.bssid ?? qsTr("Unknown") + } - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Security") - } + PropertyRow { + showTopMargin: true + label: qsTr("Signal strength") + value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A") + } - StyledText { - text: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } + PropertyRow { + showTopMargin: true + label: qsTr("Frequency") + value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A") } - } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Connection information") - font.pointSize: Appearance.font.size.larger - font.weight: 500 + PropertyRow { + showTopMargin: true + label: qsTr("Security") + value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A") + } } - StyledText { - text: qsTr("Network connection details") - color: Colours.palette.m3outline + SectionHeader { + title: qsTr("Connection information") + description: qsTr("Network connection details") } - StyledRect { - Layout.fillWidth: true - implicitHeight: connectionInfo.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: connectionInfo - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.small / 2 - - StyledText { - text: qsTr("IP Address") - } - - StyledText { - text: Network.wirelessDeviceDetails?.ipAddress || qsTr("Not available") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Subnet Mask") - } - - StyledText { - text: Network.wirelessDeviceDetails?.subnet || qsTr("Not available") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Gateway") - } - - StyledText { - text: Network.wirelessDeviceDetails?.gateway || qsTr("Not available") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("DNS Servers") - } - - StyledText { - text: (Network.wirelessDeviceDetails && Network.wirelessDeviceDetails.dns && Network.wirelessDeviceDetails.dns.length > 0) ? Network.wirelessDeviceDetails.dns.join(", ") : qsTr("Not available") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - wrapMode: Text.Wrap - Layout.maximumWidth: parent.width - } + SectionContainer { + ConnectionInfoSection { + deviceDetails: Network.wirelessDeviceDetails } } } } - component Toggle: RowLayout { - required property string label - property alias checked: toggle.checked - property alias toggle: toggle - - Layout.fillWidth: true - spacing: Appearance.spacing.normal +} - StyledText { - Layout.fillWidth: true - text: parent.label - } - StyledSwitch { - id: toggle - cLayer: 2 - } - } -} diff --git a/plan.plan.md b/plan.plan.md new file mode 100644 index 0000000..5b6f1f2 --- /dev/null +++ b/plan.plan.md @@ -0,0 +1,113 @@ +# Refactoring Plan: Control Center Panes + +## Overview + +After analyzing the last 30 commits, I've identified significant code duplication and opportunities for modularization in the control center panels. This plan focuses on extracting common patterns into reusable components. + +## Key Refactoring Opportunities + +### 1. Details Component Consolidation + +**Files affected:** `modules/controlcenter/network/Details.qml`, `modules/controlcenter/ethernet/EthernetDetails.qml` + +**Issue:** Both files share identical structure: + +- Header with icon and title +- Connection status section +- Properties section +- Connection information section (IP, subnet, gateway, DNS) + +**Solution:** Create `components/ConnectionDetails.qml` that accepts: + +- Device/network object +- Icon name +- Title property path +- Details source (wirelessDeviceDetails vs ethernetDeviceDetails) + +**Impact:** Reduces ~200 lines of duplication. + +### 2. ToggleButton Component Extraction + +**Files affected:** `modules/controlcenter/network/NetworkList.qml`, `modules/controlcenter/ethernet/EthernetList.qml` + +**Issue:** Both files define identical `ToggleButton` component (lines 228-301 in NetworkList, 170-243 in EthernetList). + +**Solution:** Move to `components/controls/ToggleButton.qml` and import in both files. + +**Impact:** Eliminates ~70 lines of duplication. + +### 3. Switch/SpinBox Row Components + +**Files affected:** `modules/controlcenter/appearance/AppearancePane.qml`, `modules/controlcenter/taskbar/TaskbarPane.qml` + +**Issue:** Repeated patterns for: + +- Switch rows (label + StyledSwitch) +- SpinBox rows (label + CustomSpinBox) +- Same layout, spacing, and styling + +**Solution:** Create: + +- `components/controls/SwitchRow.qml` - label + switch with config save callback +- `components/controls/SpinBoxRow.qml` - label + spinbox with config save callback + +**Impact:** Reduces ~30-40 lines per row instance (20+ instances total). + +### 4. Font List Delegate Consolidation + +**Files affected:** `modules/controlcenter/appearance/AppearancePane.qml` + +**Issue:** Three nearly identical font list implementations (Material, Mono, Sans) with only the property binding differing. + +**Solution:** Create `components/FontList.qml` that accepts: + +- Current font property +- Save callback function +- Title text + +**Impact:** Reduces ~150 lines of duplication. + +### 5. List Item Selection Pattern + +**Files affected:** Multiple list delegates across panes + +**Issue:** Repeated pattern for selected item highlighting: + +- Color with alpha based on selection +- Border width/color based on selection +- StateLayer click handler + +**Solution:** Create `components/SelectableListItem.qml` wrapper that handles selection styling. + +**Impact:** Reduces ~10-15 lines per list delegate. + +## Implementation Order + +1. **ConnectionDetails consolidation** (medium impact) +2. **FontList consolidation** (low-medium impact) +3. **SelectableListItem pattern** (nice-to-have, lower priority) + +## Files to Create + +- `components/controls/SelectableListItem.qml` +- `components/ConnectionDetails.qml` +- `components/FontList.qml` + +## Completed Items + +- ✅ `components/controls/CollapsibleSection.qml` - DONE +- ✅ `components/controls/SwitchRow.qml` - DONE +- ✅ `components/controls/SpinBoxRow.qml` - DONE +- ✅ `components/controls/ToggleButton.qml` - DONE + +## Estimated Impact + +- **Lines removed:** ~400-500 lines of duplicated code (from remaining items) +- **Maintainability:** Significantly improved - changes to common patterns only need to be made once +- **Readability:** Panes become more declarative and easier to understand +- **Testability:** Reusable components can be tested independently + +## Completed Refactoring + +- **Lines removed so far:** ~1300+ lines of duplicated code +- **Components created:** CollapsibleSection, SwitchRow, SpinBoxRow, ToggleButton \ No newline at end of file -- cgit v1.2.3-freya From 223159f5b74fa53c4fe37cea40ca68afd2727823 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 16:42:45 -0500 Subject: notifs/toasts: refactoring --- modules/notifications/AppIconBadge.qml | 72 ++++++++++++++++++ modules/notifications/Notification.qml | 59 ++------------- modules/notifications/NotificationToast.qml | 44 ++--------- plan.plan.md | 113 ---------------------------- 4 files changed, 84 insertions(+), 204 deletions(-) create mode 100644 modules/notifications/AppIconBadge.qml delete mode 100644 plan.plan.md diff --git a/modules/notifications/AppIconBadge.qml b/modules/notifications/AppIconBadge.qml new file mode 100644 index 0000000..8bbae89 --- /dev/null +++ b/modules/notifications/AppIconBadge.qml @@ -0,0 +1,72 @@ +import qs.components +import qs.components.effects +import qs.services +import qs.config +import qs.utils +import Quickshell +import Quickshell.Services.Notifications +import QtQuick + +StyledRect { + id: root + + required property Notifs.Notif modelData + required property bool hasImage + required property bool hasAppIcon + required property bool isCritical + required property bool isLow + + radius: Appearance.rounding.full + color: { + if (root.isCritical) return Colours.palette.m3error; + if (root.isLow) return Colours.layer(Colours.palette.m3surfaceContainerHighest, 2); + return Colours.palette.m3secondaryContainer; + } + implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + + Loader { + id: icon + + active: root.hasAppIcon + asynchronous: false + visible: active + + anchors.centerIn: parent + + width: Math.round(parent.width * 0.6) + height: Math.round(parent.width * 0.6) + + sourceComponent: ColouredIcon { + anchors.fill: parent + source: Quickshell.iconPath(root.modelData.appIcon) + colour: { + if (root.isCritical) return Colours.palette.m3onError; + if (root.isLow) return Colours.palette.m3onSurface; + return Colours.palette.m3onSecondaryContainer; + } + layer.enabled: root.modelData.appIcon.endsWith("symbolic") + } + } + + Loader { + active: !root.hasAppIcon + asynchronous: false + visible: active + anchors.centerIn: parent + anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 + anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 + + sourceComponent: MaterialIcon { + text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) + + color: { + if (root.isCritical) return Colours.palette.m3onError; + if (root.isLow) return Colours.palette.m3onSurface; + return Colours.palette.m3onSecondaryContainer; + } + font.pointSize: Appearance.font.size.large + } + } +} + diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 091da2c..75defb6 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -155,59 +155,12 @@ StyledRect { anchors.right: root.hasImage ? image.right : undefined anchors.bottom: root.hasImage ? image.bottom : undefined - sourceComponent: StyledRect { - radius: Appearance.rounding.full - color: { - if (root.isCritical) return Colours.palette.m3error; - if (root.isLow) return Colours.layer(Colours.palette.m3surfaceContainerHighest, 2); - return Colours.palette.m3secondaryContainer; - } - implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image - implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image - - Loader { - id: icon - - active: root.hasAppIcon - asynchronous: false - visible: active - - anchors.centerIn: parent - - width: Math.round(parent.width * 0.6) - height: Math.round(parent.width * 0.6) - - sourceComponent: ColouredIcon { - anchors.fill: parent - source: Quickshell.iconPath(root.modelData.appIcon) - colour: { - if (root.isCritical) return Colours.palette.m3onError; - if (root.isLow) return Colours.palette.m3onSurface; - return Colours.palette.m3onSecondaryContainer; - } - layer.enabled: root.modelData.appIcon.endsWith("symbolic") - } - } - - Loader { - active: !root.hasAppIcon - asynchronous: false - visible: active - anchors.centerIn: parent - anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 - anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 - - sourceComponent: MaterialIcon { - text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) - - color: { - if (root.isCritical) return Colours.palette.m3onError; - if (root.isLow) return Colours.palette.m3onSurface; - return Colours.palette.m3onSecondaryContainer; - } - font.pointSize: Appearance.font.size.large - } - } + sourceComponent: AppIconBadge { + modelData: root.modelData + hasImage: root.hasImage + hasAppIcon: root.hasAppIcon + isCritical: root.isCritical + isLow: root.isLow } } diff --git a/modules/notifications/NotificationToast.qml b/modules/notifications/NotificationToast.qml index 1ce334b..f8b830a 100644 --- a/modules/notifications/NotificationToast.qml +++ b/modules/notifications/NotificationToast.qml @@ -81,44 +81,12 @@ StyledRect { anchors.right: root.hasImage ? parent.right : undefined anchors.bottom: root.hasImage ? parent.bottom : undefined - sourceComponent: StyledRect { - radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer - implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image - implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image - - Loader { - id: appIcon - - active: root.hasAppIcon - asynchronous: true - - anchors.centerIn: parent - - width: Math.round(parent.width * 0.6) - height: Math.round(parent.width * 0.6) - - sourceComponent: ColouredIcon { - anchors.fill: parent - source: Quickshell.iconPath(root.modelData.appIcon) - colour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - layer.enabled: root.modelData.appIcon.endsWith("symbolic") - } - } - - Loader { - active: !root.hasAppIcon - asynchronous: true - anchors.centerIn: parent - anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 - anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 - - sourceComponent: MaterialIcon { - text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large - } - } + sourceComponent: AppIconBadge { + modelData: root.modelData + hasImage: root.hasImage + hasAppIcon: root.hasAppIcon + isCritical: root.modelData.urgency === NotificationUrgency.Critical + isLow: root.modelData.urgency === NotificationUrgency.Low } } } diff --git a/plan.plan.md b/plan.plan.md deleted file mode 100644 index 5b6f1f2..0000000 --- a/plan.plan.md +++ /dev/null @@ -1,113 +0,0 @@ -# Refactoring Plan: Control Center Panes - -## Overview - -After analyzing the last 30 commits, I've identified significant code duplication and opportunities for modularization in the control center panels. This plan focuses on extracting common patterns into reusable components. - -## Key Refactoring Opportunities - -### 1. Details Component Consolidation - -**Files affected:** `modules/controlcenter/network/Details.qml`, `modules/controlcenter/ethernet/EthernetDetails.qml` - -**Issue:** Both files share identical structure: - -- Header with icon and title -- Connection status section -- Properties section -- Connection information section (IP, subnet, gateway, DNS) - -**Solution:** Create `components/ConnectionDetails.qml` that accepts: - -- Device/network object -- Icon name -- Title property path -- Details source (wirelessDeviceDetails vs ethernetDeviceDetails) - -**Impact:** Reduces ~200 lines of duplication. - -### 2. ToggleButton Component Extraction - -**Files affected:** `modules/controlcenter/network/NetworkList.qml`, `modules/controlcenter/ethernet/EthernetList.qml` - -**Issue:** Both files define identical `ToggleButton` component (lines 228-301 in NetworkList, 170-243 in EthernetList). - -**Solution:** Move to `components/controls/ToggleButton.qml` and import in both files. - -**Impact:** Eliminates ~70 lines of duplication. - -### 3. Switch/SpinBox Row Components - -**Files affected:** `modules/controlcenter/appearance/AppearancePane.qml`, `modules/controlcenter/taskbar/TaskbarPane.qml` - -**Issue:** Repeated patterns for: - -- Switch rows (label + StyledSwitch) -- SpinBox rows (label + CustomSpinBox) -- Same layout, spacing, and styling - -**Solution:** Create: - -- `components/controls/SwitchRow.qml` - label + switch with config save callback -- `components/controls/SpinBoxRow.qml` - label + spinbox with config save callback - -**Impact:** Reduces ~30-40 lines per row instance (20+ instances total). - -### 4. Font List Delegate Consolidation - -**Files affected:** `modules/controlcenter/appearance/AppearancePane.qml` - -**Issue:** Three nearly identical font list implementations (Material, Mono, Sans) with only the property binding differing. - -**Solution:** Create `components/FontList.qml` that accepts: - -- Current font property -- Save callback function -- Title text - -**Impact:** Reduces ~150 lines of duplication. - -### 5. List Item Selection Pattern - -**Files affected:** Multiple list delegates across panes - -**Issue:** Repeated pattern for selected item highlighting: - -- Color with alpha based on selection -- Border width/color based on selection -- StateLayer click handler - -**Solution:** Create `components/SelectableListItem.qml` wrapper that handles selection styling. - -**Impact:** Reduces ~10-15 lines per list delegate. - -## Implementation Order - -1. **ConnectionDetails consolidation** (medium impact) -2. **FontList consolidation** (low-medium impact) -3. **SelectableListItem pattern** (nice-to-have, lower priority) - -## Files to Create - -- `components/controls/SelectableListItem.qml` -- `components/ConnectionDetails.qml` -- `components/FontList.qml` - -## Completed Items - -- ✅ `components/controls/CollapsibleSection.qml` - DONE -- ✅ `components/controls/SwitchRow.qml` - DONE -- ✅ `components/controls/SpinBoxRow.qml` - DONE -- ✅ `components/controls/ToggleButton.qml` - DONE - -## Estimated Impact - -- **Lines removed:** ~400-500 lines of duplicated code (from remaining items) -- **Maintainability:** Significantly improved - changes to common patterns only need to be made once -- **Readability:** Panes become more declarative and easier to understand -- **Testability:** Reusable components can be tested independently - -## Completed Refactoring - -- **Lines removed so far:** ~1300+ lines of duplicated code -- **Components created:** CollapsibleSection, SwitchRow, SpinBoxRow, ToggleButton \ No newline at end of file -- cgit v1.2.3-freya From 84e839cf55fe745185c33b2d597501bcababa547 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 16:50:20 -0500 Subject: notif/toasts: refactoring colors --- modules/notifications/AppIconBadge.qml | 21 +++-------------- modules/notifications/Notification.qml | 26 +++++---------------- modules/notifications/NotificationToast.qml | 2 -- services/Notifs.qml | 35 +++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 41 deletions(-) diff --git a/modules/notifications/AppIconBadge.qml b/modules/notifications/AppIconBadge.qml index 8bbae89..286522f 100644 --- a/modules/notifications/AppIconBadge.qml +++ b/modules/notifications/AppIconBadge.qml @@ -13,15 +13,9 @@ StyledRect { required property Notifs.Notif modelData required property bool hasImage required property bool hasAppIcon - required property bool isCritical - required property bool isLow radius: Appearance.rounding.full - color: { - if (root.isCritical) return Colours.palette.m3error; - if (root.isLow) return Colours.layer(Colours.palette.m3surfaceContainerHighest, 2); - return Colours.palette.m3secondaryContainer; - } + color: modelData.getBadgeBackgroundColor() implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image @@ -40,11 +34,7 @@ StyledRect { sourceComponent: ColouredIcon { anchors.fill: parent source: Quickshell.iconPath(root.modelData.appIcon) - colour: { - if (root.isCritical) return Colours.palette.m3onError; - if (root.isLow) return Colours.palette.m3onSurface; - return Colours.palette.m3onSecondaryContainer; - } + colour: root.modelData.getIconColor() layer.enabled: root.modelData.appIcon.endsWith("symbolic") } } @@ -59,12 +49,7 @@ StyledRect { sourceComponent: MaterialIcon { text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) - - color: { - if (root.isCritical) return Colours.palette.m3onError; - if (root.isLow) return Colours.palette.m3onSurface; - return Colours.palette.m3onSecondaryContainer; - } + color: root.modelData.getIconColor() font.pointSize: Appearance.font.size.large } } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 75defb6..bc5c086 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -17,8 +17,6 @@ StyledRect { required property Notifs.Notif modelData readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 - readonly property bool isCritical: modelData.urgency === NotificationUrgency.Critical - readonly property bool isLow: modelData.urgency === NotificationUrgency.Low readonly property int nonAnimHeight: { const baseHeight = summary.implicitHeight + inner.anchors.margins * 2; return root.expanded @@ -28,9 +26,7 @@ StyledRect { property bool expanded property bool disableSlideIn: false - color: root.isCritical - ? Colours.palette.m3secondaryContainer - : Colours.tPalette.m3surfaceContainer + color: modelData.getBackgroundColor() radius: Appearance.rounding.normal implicitWidth: Config.notifs.sizes.width implicitHeight: inner.implicitHeight @@ -159,8 +155,6 @@ StyledRect { modelData: root.modelData hasImage: root.hasImage hasAppIcon: root.hasAppIcon - isCritical: root.isCritical - isLow: root.isLow } } @@ -302,9 +296,7 @@ StyledRect { StateLayer { radius: Appearance.rounding.full - color: root.isCritical - ? Colours.palette.m3onSecondaryContainer - : Colours.palette.m3onSurface + color: root.modelData.getStateLayerColor() function onClicked() { root.expanded = !root.expanded; @@ -424,12 +416,8 @@ StyledRect { required property var modelData - readonly property bool isCritical: root.isCritical - radius: Appearance.rounding.full - color: isCritical - ? Colours.palette.m3secondary - : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + color: root.modelData.getActionBackgroundColor() Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2 Layout.preferredHeight: actionText.height + Appearance.padding.small * 2 @@ -438,9 +426,7 @@ StyledRect { StateLayer { radius: Appearance.rounding.full - color: isCritical - ? Colours.palette.m3onSecondary - : Colours.palette.m3onSurface + color: root.modelData.getStateLayerColor() function onClicked(): void { action.modelData.invoke(); @@ -452,9 +438,7 @@ StyledRect { anchors.centerIn: parent text: actionTextMetrics.elidedText - color: isCritical - ? Colours.palette.m3onSecondary - : Colours.palette.m3onSurfaceVariant + color: root.modelData.getActionTextColor() font.pointSize: Appearance.font.size.small } diff --git a/modules/notifications/NotificationToast.qml b/modules/notifications/NotificationToast.qml index f8b830a..90414fe 100644 --- a/modules/notifications/NotificationToast.qml +++ b/modules/notifications/NotificationToast.qml @@ -85,8 +85,6 @@ StyledRect { modelData: root.modelData hasImage: root.hasImage hasAppIcon: root.hasAppIcon - isCritical: root.modelData.urgency === NotificationUrgency.Critical - isLow: root.modelData.urgency === NotificationUrgency.Low } } } diff --git a/services/Notifs.qml b/services/Notifs.qml index ea0c52a..82ed8c4 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -179,6 +179,41 @@ Singleton { property bool hasActionIcons property list actions + readonly property bool isCritical: urgency === NotificationUrgency.Critical + readonly property bool isLow: urgency === NotificationUrgency.Low + + function getBackgroundColor(): color { + if (isCritical) return Colours.palette.m3secondaryContainer; + return Colours.tPalette.m3surfaceContainer; + } + + function getBadgeBackgroundColor(): color { + if (isCritical) return Colours.palette.m3error; + if (isLow) return Colours.layer(Colours.palette.m3surfaceContainerHighest, 2); + return Colours.palette.m3secondaryContainer; + } + + function getIconColor(): color { + if (isCritical) return Colours.palette.m3onError; + if (isLow) return Colours.palette.m3onSurface; + return Colours.palette.m3onSecondaryContainer; + } + + function getStateLayerColor(): color { + if (isCritical) return Colours.palette.m3onSecondaryContainer; + return Colours.palette.m3onSurface; + } + + function getActionBackgroundColor(): color { + if (isCritical) return Colours.palette.m3secondary; + return Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); + } + + function getActionTextColor(): color { + if (isCritical) return Colours.palette.m3onSecondary; + return Colours.palette.m3onSurfaceVariant; + } + readonly property Timer timer: Timer { id: toastTimer -- cgit v1.2.3-freya From 7e6f3270911d9a3b7a73532b18670a5fa613ee92 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 19:57:42 -0500 Subject: conrolcenter: debug/rewrite of wireless panel --- modules/bar/popouts/Network.qml | 2 +- modules/controlcenter/network/Details.qml | 55 +++- modules/controlcenter/network/NetworkList.qml | 5 +- modules/controlcenter/network/PasswordDialog.qml | 283 +++++++++++++++++++- services/Network.qml | 316 ++++++++++++++++++++++- 5 files changed, 637 insertions(+), 24 deletions(-) diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index f21a92d..f040b6a 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -118,7 +118,7 @@ ColumnLayout { Network.disconnectFromNetwork(); } else { root.connectingToSsid = networkItem.modelData.ssid; - Network.connectToNetwork(networkItem.modelData.ssid, ""); + Network.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); } } } diff --git a/modules/controlcenter/network/Details.qml b/modules/controlcenter/network/Details.qml index a53f62e..5e636a2 100644 --- a/modules/controlcenter/network/Details.qml +++ b/modules/controlcenter/network/Details.qml @@ -95,10 +95,31 @@ Item { // Callback: connection failed, show password dialog root.session.network.showPasswordDialog = true; root.session.network.pendingNetwork = root.network; - } + }, + root.network.bssid ); } else { - Network.connectToNetwork(root.network.ssid, ""); + Network.connectToNetwork(root.network.ssid, "", root.network.bssid, null); + } + } + } + + Button { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + visible: root.network && root.network.ssid && Network.savedConnections.includes(root.network.ssid) + color: Colours.palette.m3errorContainer + onColor: Colours.palette.m3onErrorContainer + text: qsTr("Forget Network") + + onClicked: { + if (root.network && root.network.ssid) { + // Disconnect first if connected + if (root.network.active) { + Network.disconnectFromNetwork(); + } + // Delete the connection profile + Network.forgetNetwork(root.network.ssid); } } } @@ -152,9 +173,39 @@ Item { deviceDetails: Network.wirelessDeviceDetails } } + } } + component Button: StyledRect { + property color onColor: Colours.palette.m3onSurface + property alias disabled: stateLayer.disabled + property alias text: label.text + property alias enabled: stateLayer.enabled + + Layout.fillWidth: true + implicitHeight: label.implicitHeight + Appearance.padding.normal * 2 + radius: Appearance.rounding.normal + + StateLayer { + id: stateLayer + color: parent.onColor + function onClicked(): void { + if (parent.enabled !== false) { + parent.clicked(); + } + } + } + + StyledText { + id: label + anchors.centerIn: parent + color: parent.onColor + } + + signal clicked + } + } diff --git a/modules/controlcenter/network/NetworkList.qml b/modules/controlcenter/network/NetworkList.qml index 09d7352..6c4158c 100644 --- a/modules/controlcenter/network/NetworkList.qml +++ b/modules/controlcenter/network/NetworkList.qml @@ -203,10 +203,11 @@ ColumnLayout { // Callback: connection failed, show password dialog root.session.network.showPasswordDialog = true; root.session.network.pendingNetwork = modelData; - } + }, + modelData.bssid ); } else { - Network.connectToNetwork(modelData.ssid, ""); + Network.connectToNetwork(modelData.ssid, "", modelData.bssid, null); } } } diff --git a/modules/controlcenter/network/PasswordDialog.qml b/modules/controlcenter/network/PasswordDialog.qml index fa4788c..7aa698b 100644 --- a/modules/controlcenter/network/PasswordDialog.qml +++ b/modules/controlcenter/network/PasswordDialog.qml @@ -14,11 +14,37 @@ Item { id: root required property Session session - readonly property var network: session.network.pendingNetwork + readonly property var network: { + // Try pendingNetwork first, then fall back to active network selection + if (session.network.pendingNetwork) { + return session.network.pendingNetwork; + } + // Fallback to active network if available + if (session.network.active) { + return session.network.active; + } + return null; + } visible: session.network.showPasswordDialog enabled: visible focus: visible + + // Ensure network is set when dialog opens + Component.onCompleted: { + if (visible && !session.network.pendingNetwork && session.network.active) { + session.network.pendingNetwork = session.network.active; + } + } + + Connections { + target: root + function onVisibleChanged(): void { + if (visible && !session.network.pendingNetwork && session.network.active) { + session.network.pendingNetwork = session.network.active; + } + } + } Keys.onEscapePressed: { root.session.network.showPasswordDialog = false; @@ -105,6 +131,35 @@ Item { font.pointSize: Appearance.font.size.small } + StyledText { + id: statusText + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.spacing.small + visible: Network.connectionStatus.length > 0 || connectButton.connecting + text: { + if (Network.connectionStatus.length > 0) { + return Network.connectionStatus; + } else if (connectButton.connecting) { + return qsTr("Connecting..."); + } + return ""; + } + color: { + const status = Network.connectionStatus; + if (status.includes("Error") || status.includes("error") || status.includes("failed")) { + return Colours.palette.m3error; + } else if (status.includes("successful") || status.includes("Connected") || status.includes("success")) { + return Colours.palette.m3primary; + } + return Colours.palette.m3onSurfaceVariant; + } + font.pointSize: Appearance.font.size.small + font.weight: (Network.connectionStatus.includes("Error") || Network.connectionStatus.includes("error")) ? 500 : 400 + wrapMode: Text.WordWrap + Layout.maximumWidth: parent.width - Appearance.padding.large * 2 + } + Item { Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true @@ -145,9 +200,17 @@ Item { if (root.visible) { passwordField.forceActiveFocus(); passwordField.text = ""; + Network.connectionStatus = ""; } } } + + Connections { + target: Network + function onConnectionStatusChanged(): void { + // Status updated, ensure it's visible + } + } Keys.onReturnPressed: { if (connectButton.enabled) { @@ -190,12 +253,222 @@ Item { text: qsTr("Connect") enabled: passwordField.text.length > 0 + property bool connecting: false + function onClicked(): void { - if (root.network && passwordField.text.length > 0) { - Network.connectToNetwork(root.network.ssid, passwordField.text); - root.session.network.showPasswordDialog = false; - passwordField.text = ""; + Network.connectionStatus = ""; + + // Get password first + const password = passwordField.text; + + // Try multiple ways to get the network + let networkToUse = null; + + // Try 1: root.network (computed property) + if (root.network) { + networkToUse = root.network; + } + + // Try 2: pendingNetwork + if (!networkToUse && root.session.network.pendingNetwork) { + networkToUse = root.session.network.pendingNetwork; + } + + // Try 3: active network + if (!networkToUse && root.session.network.active) { + networkToUse = root.session.network.active; + root.session.network.pendingNetwork = networkToUse; + } + + // Check all conditions + const hasNetwork = !!networkToUse; + const hasPassword = password && password.length > 0; + const notConnecting = !connecting; + + if (hasNetwork && hasPassword && notConnecting) { + // Set status immediately + Network.connectionStatus = qsTr("Preparing to connect..."); + + // Keep dialog open and track connection + connecting = true; + connectButton.enabled = false; + connectButton.text = qsTr("Connecting..."); + + // Force immediate UI update + statusText.visible = true; + + // Store target SSID for later comparison + const ssidToConnect = networkToUse.ssid || ""; + const bssidToConnect = networkToUse.bssid || ""; + + // Store the SSID we're connecting to so we can compare later + // even if root.network changes + content.connectingToSsid = ssidToConnect; + + // Execute connection immediately + Network.connectToNetwork( + ssidToConnect, + password, + bssidToConnect, + () => { + // Callback if connection fails - keep dialog open + connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + content.connectingToSsid = ""; // Clear on failure + } + ); + + // Start connection check timer immediately + connectionCheckTimer.checkCount = 0; + connectionCheckTimer.start(); + + // Also check immediately after a short delay to catch quick connections + Qt.callLater(() => { + if (root.visible) { + closeDialogIfConnected(); + } + }); + } else { + // Show error in status + Network.connectionStatus = qsTr("Error: Cannot connect - missing network or password"); + } + } + } + } + + // Store the SSID we're connecting to when connection starts + property string connectingToSsid: "" + + property string targetSsid: { + // Track the SSID we're trying to connect to + // Prefer explicitly stored connectingToSsid, then computed values + if (connectingToSsid && connectingToSsid.length > 0) { + return connectingToSsid; + } + if (root.network && root.network.ssid) { + return root.network.ssid; + } + if (root.session.network.pendingNetwork && root.session.network.pendingNetwork.ssid) { + return root.session.network.pendingNetwork.ssid; + } + return ""; + } + + function closeDialogIfConnected(): bool { + // Check if we're connected to the network we're trying to connect to + const ssid = targetSsid; + + if (!ssid || ssid.length === 0) { + return false; + } + + if (!Network.active) { + return false; + } + + const activeSsid = Network.active.ssid || ""; + + if (activeSsid === ssid) { + // Connection succeeded - close dialog + connectionCheckTimer.stop(); + aggressiveCheckTimer.stop(); + connectionCheckTimer.checkCount = 0; + connectButton.connecting = false; + Network.connectionStatus = ""; + root.session.network.showPasswordDialog = false; + passwordField.text = ""; + content.connectingToSsid = ""; // Clear stored SSID + return true; + } + return false; + } + + Timer { + id: connectionCheckTimer + interval: 1000 // Check every 1 second for faster response + repeat: true + triggeredOnStart: false + property int checkCount: 0 + + onTriggered: { + checkCount++; + + // Try to close dialog if connected + const closed = content.closeDialogIfConnected(); + if (closed) { + return; + } + + if (connectButton.connecting) { + // Still connecting, check again + // Limit to 20 checks (20 seconds total) + if (checkCount >= 20) { + connectionCheckTimer.stop(); + connectionCheckTimer.checkCount = 0; + connectButton.connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + } + } else { + // Not connecting anymore, stop timer + connectionCheckTimer.stop(); + connectionCheckTimer.checkCount = 0; + } + } + } + + Connections { + target: Network + function onActiveChanged(): void { + // Check immediately when active network changes + if (root.visible) { + // Check immediately - if connected, close right away + if (content.closeDialogIfConnected()) { + return; + } + + // Also check after a delay in case the active network isn't fully updated yet + Qt.callLater(() => { + if (root.visible) { + content.closeDialogIfConnected(); + } + }); + } + } + } + + // Also check when dialog becomes visible + Connections { + target: root + function onVisibleChanged(): void { + if (root.visible) { + // Check immediately when dialog opens + Qt.callLater(() => { + if (root.visible) { + closeDialogIfConnected(); + } + }); + } + } + } + + // Aggressive polling timer - checks every 500ms when dialog is visible and connecting + // This ensures we catch the connection even if signals are missed + Timer { + id: aggressiveCheckTimer + interval: 500 + repeat: true + running: root.visible && connectButton.connecting + triggeredOnStart: true + + onTriggered: { + if (root.visible && connectButton.connecting) { + if (content.closeDialogIfConnected()) { + stop(); } + } else { + stop(); } } } diff --git a/services/Network.qml b/services/Network.qml index c8ab264..ea5c3e7 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -12,6 +12,8 @@ Singleton { Qt.callLater(() => { getEthernetDevices(); }); + // Load saved connections on startup + listConnectionsProc.running = true; } readonly property list networks: [] @@ -26,6 +28,34 @@ Singleton { property bool ethernetProcessRunning: false property var ethernetDeviceDetails: null property var wirelessDeviceDetails: null + property string connectionStatus: "" + property string connectionDebug: "" + + function clearConnectionStatus(): void { + connectionStatus = ""; + // Don't clear debug - keep it for reference + // connectionDebug = ""; + } + + function setConnectionStatus(status: string): void { + connectionStatus = status; + } + + function addDebugInfo(info: string): void { + const timestamp = new Date().toLocaleTimeString(); + const newInfo = "[" + timestamp + "] " + info; + // CRITICAL: Always append - NEVER replace + // Get current value - NEVER allow it to be empty/cleared + let current = connectionDebug; + if (!current || current === undefined || current === null) { + current = ""; + } + // ALWAYS append - never replace + // If current is empty, just use newInfo, otherwise append with newline + const updated = (current.length > 0) ? (current + "\n" + newInfo) : newInfo; + // CRITICAL: Only assign if we're appending, never replace + connectionDebug = updated; + } function enableWifi(enabled: bool): void { const cmd = enabled ? "on" : "off"; @@ -44,30 +74,118 @@ Singleton { property var pendingConnection: null signal connectionFailed(string ssid) - function connectToNetwork(ssid: string, password: string): void { - // First try to connect to an existing connection - // If that fails, create a new connection + function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { + // When password is provided, use BSSID for more reliable connection + // When no password, use SSID (will use saved password if available) + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + let cmd = []; + + // Set up pending connection tracking if callback provided + if (callback) { + root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; + } + if (password && password.length > 0) { - connectProc.exec(["nmcli", "device", "wifi", "connect", ssid, "password", password]); + // When password is provided, try BSSID first if available, otherwise use SSID + if (hasBssid) { + // Use BSSID when password is provided - ensure BSSID is uppercase + const bssidUpper = bssid.toUpperCase(); + // Create connection profile with all required properties for BSSID + password + // First remove any existing connection with this name + cmd = ["nmcli", "connection", "add", + "type", "wifi", + "con-name", ssid, + "ifname", "*", + "ssid", ssid, + "802-11-wireless.bssid", bssidUpper, + "802-11-wireless-security.key-mgmt", "wpa-psk", + "802-11-wireless-security.psk", password]; + root.setConnectionStatus(qsTr("Connecting to %1 (BSSID: %2)...").arg(ssid).arg(bssidUpper)); + root.addDebugInfo(qsTr("Using BSSID: %1 for SSID: %2").arg(bssidUpper).arg(ssid)); + root.addDebugInfo(qsTr("Creating connection profile with password and key-mgmt")); + } else { + // Fallback to SSID if BSSID not available - use device wifi connect + cmd = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; + root.setConnectionStatus(qsTr("Connecting to %1...").arg(ssid)); + root.addDebugInfo(qsTr("Using SSID only (no BSSID): %1").arg(ssid)); + } } else { // Try to connect to existing connection first (will use saved password if available) - connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); + cmd = ["nmcli", "device", "wifi", "connect", ssid]; + root.setConnectionStatus(qsTr("Connecting to %1 (using saved password)...").arg(ssid)); + root.addDebugInfo(qsTr("Using saved password for: %1").arg(ssid)); + } + + // Show the exact command being executed + const cmdStr = cmd.join(" "); + root.addDebugInfo(qsTr("=== COMMAND TO EXECUTE ===")); + root.addDebugInfo(qsTr("Command: %1").arg(cmdStr)); + root.addDebugInfo(qsTr("Command array: [%1]").arg(cmd.map((arg, i) => `"${arg}"`).join(", "))); + root.addDebugInfo(qsTr("Command array length: %1").arg(cmd.length)); + root.addDebugInfo(qsTr("===========================")); + + // Set command and start process + root.addDebugInfo(qsTr("Setting command property...")); + connectProc.command = cmd; + const setCmdStr = connectProc.command ? connectProc.command.join(" ") : "null"; + root.addDebugInfo(qsTr("Command property set, value: %1").arg(setCmdStr)); + root.addDebugInfo(qsTr("Command property verified: %1").arg(setCmdStr === cmdStr ? "Match" : "MISMATCH")); + + // If we're creating a connection profile, we need to activate it after creation + const isConnectionAdd = cmd.length > 0 && cmd[0] === "nmcli" && cmd[1] === "connection" && cmd[2] === "add"; + + // Wait a moment before starting to ensure command is set + Qt.callLater(() => { + root.addDebugInfo(qsTr("=== STARTING PROCESS ===")); + root.addDebugInfo(qsTr("Current running state: %1").arg(connectProc.running)); + root.addDebugInfo(qsTr("Command to run: %1").arg(connectProc.command ? connectProc.command.join(" ") : "NOT SET")); + root.addDebugInfo(qsTr("Is connection add command: %1").arg(isConnectionAdd)); + connectProc.running = true; + root.addDebugInfo(qsTr("Process running set to: %1").arg(connectProc.running)); + root.addDebugInfo(qsTr("========================")); + + // Check if process actually started after a short delay + Qt.callLater(() => { + root.addDebugInfo(qsTr("Process status check (100ms later):")); + root.addDebugInfo(qsTr(" Running: %1").arg(connectProc.running)); + root.addDebugInfo(qsTr(" Command: %1").arg(connectProc.command ? connectProc.command.join(" ") : "null")); + if (!connectProc.running) { + root.addDebugInfo(qsTr("WARNING: Process did not start!")); + root.setConnectionStatus(qsTr("Error: Process failed to start")); + } + }, 100); + }); + + // Start connection check timer if we have a callback + if (callback) { + root.addDebugInfo(qsTr("Starting connection check timer (4 second interval)")); + connectionCheckTimer.start(); + } else { + root.addDebugInfo(qsTr("No callback provided - not starting connection check timer")); } } - function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var): void { + function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { + root.addDebugInfo(qsTr("=== connectToNetworkWithPasswordCheck ===")); + root.addDebugInfo(qsTr("SSID: %1, isSecure: %2").arg(ssid).arg(isSecure)); + // For secure networks, try connecting without password first // If connection succeeds (saved password exists), we're done // If it fails with password error, callback will be called to show password dialog if (isSecure) { - root.pendingConnection = { ssid: ssid, callback: callback }; + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; + root.addDebugInfo(qsTr("Trying to connect without password (will use saved if available)")); // Try connecting without password - will use saved password if available connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); // Start timer to check if connection succeeded + root.addDebugInfo(qsTr("Starting connection check timer")); connectionCheckTimer.start(); } else { - connectToNetwork(ssid, ""); + root.addDebugInfo(qsTr("Network is not secure, connecting directly")); + connectToNetwork(ssid, "", bssid, null); } + root.addDebugInfo(qsTr("=========================================")); } function disconnectFromNetwork(): void { @@ -80,6 +198,50 @@ Singleton { disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); } } + + function forgetNetwork(ssid: string): void { + // Delete the connection profile for this network + // This will remove the saved password and connection settings + if (ssid && ssid.length > 0) { + deleteConnectionProc.exec(["nmcli", "connection", "delete", ssid]); + // Also refresh network list after deletion + Qt.callLater(() => { + getNetworks.running = true; + }, 500); + } + } + + function hasConnectionProfile(ssid: string): bool { + // Check if a connection profile exists for this SSID + // This is synchronous check - returns true if connection exists + if (!ssid || ssid.length === 0) { + return false; + } + // Use nmcli to check if connection exists + // We'll use a Process to check, but for now return false + // The actual check will be done asynchronously + return false; + } + + property list savedConnections: [] + + Process { + id: listConnectionsProc + command: ["nmcli", "-t", "-f", "NAME", "connection", "show"] + onExited: { + if (exitCode === 0) { + // Parse connection names from output + const connections = stdout.text.trim().split("\n").filter(name => name.length > 0); + root.savedConnections = connections; + } + } + stdout: StdioCollector { + onStreamFinished: { + const connections = text.trim().split("\n").filter(name => name.length > 0); + root.savedConnections = connections; + } + } + } function getWifiStatus(): void { wifiStatusProc.running = true; @@ -184,11 +346,17 @@ Singleton { id: connectionCheckTimer interval: 4000 onTriggered: { + root.addDebugInfo(qsTr("=== CONNECTION CHECK TIMER (4s) ===")); if (root.pendingConnection) { - // Final check - if connection still hasn't succeeded, show password dialog const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + root.addDebugInfo(qsTr("Checking connection status...")); + root.addDebugInfo(qsTr(" Pending SSID: %1").arg(root.pendingConnection.ssid)); + root.addDebugInfo(qsTr(" Active SSID: %1").arg(root.active ? root.active.ssid : "None")); + root.addDebugInfo(qsTr(" Connected: %1").arg(connected)); + if (!connected && root.pendingConnection.callback) { // Connection didn't succeed after multiple checks, show password dialog + root.addDebugInfo(qsTr("Connection failed - calling password dialog callback")); const pending = root.pendingConnection; root.pendingConnection = null; immediateCheckTimer.stop(); @@ -196,11 +364,19 @@ Singleton { pending.callback(); } else if (connected) { // Connection succeeded, clear pending + root.addDebugInfo(qsTr("Connection succeeded!")); + root.setConnectionStatus(qsTr("Connected successfully!")); root.pendingConnection = null; immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; + } else { + root.addDebugInfo(qsTr("Still connecting...")); + root.setConnectionStatus(qsTr("Still connecting...")); } + } else { + root.addDebugInfo(qsTr("No pending connection")); } + root.addDebugInfo(qsTr("================================")); } } @@ -210,23 +386,37 @@ Singleton { repeat: true triggeredOnStart: false property int checkCount: 0 + + onRunningChanged: { + if (running) { + root.addDebugInfo(qsTr("Immediate check timer started (checks every 500ms)")); + } + } + onTriggered: { if (root.pendingConnection) { checkCount++; const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + root.addDebugInfo(qsTr("Immediate check #%1: Connected=%2").arg(checkCount).arg(connected)); + if (connected) { // Connection succeeded, stop timers and clear pending + root.addDebugInfo(qsTr("Connection succeeded on check #%1!").arg(checkCount)); + root.setConnectionStatus(qsTr("Connected successfully!")); connectionCheckTimer.stop(); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; root.pendingConnection = null; } else if (checkCount >= 6) { + root.addDebugInfo(qsTr("Checked %1 times (3 seconds) - connection taking longer").arg(checkCount)); + root.setConnectionStatus(qsTr("Connection taking longer than expected...")); // Checked 6 times (3 seconds total), connection likely failed // Stop immediate check, let the main timer handle it immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; } } else { + root.addDebugInfo(qsTr("Immediate check: No pending connection, stopping timer")); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; } @@ -236,22 +426,92 @@ Singleton { Process { id: connectProc + onRunningChanged: { + root.addDebugInfo(qsTr("Process running changed to: %1").arg(running)); + } + + onStarted: { + root.addDebugInfo(qsTr("Process started successfully")); + } + onExited: { + root.addDebugInfo(qsTr("=== PROCESS EXITED ===")); + root.addDebugInfo(qsTr("Exit code: %1").arg(exitCode)); + root.addDebugInfo(qsTr("(Exit code 0 = success, non-zero = error)")); + + // Check if this was a "connection add" command - if so, we need to activate it + const wasConnectionAdd = connectProc.command && connectProc.command.length > 0 + && connectProc.command[0] === "nmcli" + && connectProc.command[1] === "connection" + && connectProc.command[2] === "add"; + + if (wasConnectionAdd && exitCode === 0 && root.pendingConnection) { + // Connection profile was created successfully, now activate it + const ssid = root.pendingConnection.ssid; + root.addDebugInfo(qsTr("Connection profile created successfully, now activating: %1").arg(ssid)); + root.setConnectionStatus(qsTr("Activating connection...")); + + // Update saved connections list since we just created one + listConnectionsProc.running = true; + + // Activate the connection we just created + connectProc.command = ["nmcli", "connection", "up", ssid]; + Qt.callLater(() => { + connectProc.running = true; + }); + // Don't start timers yet - wait for activation to complete + return; + } + // Refresh network list after connection attempt getNetworks.running = true; // Check if connection succeeded after a short delay (network list needs to update) if (root.pendingConnection) { - immediateCheckTimer.start(); + if (exitCode === 0) { + // Process succeeded, start checking connection status + root.setConnectionStatus(qsTr("Connection command succeeded, verifying...")); + root.addDebugInfo(qsTr("Command succeeded, checking connection status...")); + root.addDebugInfo(qsTr("Starting immediate check timer (500ms intervals)")); + immediateCheckTimer.start(); + } else { + // Process failed, but wait a moment to see if connection still works + root.setConnectionStatus(qsTr("Connection command exited with code %1, checking status...").arg(exitCode)); + root.addDebugInfo(qsTr("Command exited with error code %1").arg(exitCode)); + root.addDebugInfo(qsTr("This usually means the command failed")); + root.addDebugInfo(qsTr("Checking connection status anyway...")); + root.addDebugInfo(qsTr("Starting immediate check timer (500ms intervals)")); + immediateCheckTimer.start(); + } + } else { + root.addDebugInfo(qsTr("No pending connection - not starting immediate check timer")); } + root.addDebugInfo(qsTr("======================")); } stdout: SplitParser { - onRead: getNetworks.running = true + onRead: { + getNetworks.running = true; + // Also log output for debugging + if (text && text.trim().length > 0) { + root.addDebugInfo(qsTr("STDOUT: %1").arg(text.trim())); + root.setConnectionStatus(qsTr("Status: %1").arg(text.trim())); + } + } } stderr: StdioCollector { onStreamFinished: { const error = text.trim(); + root.addDebugInfo(qsTr("=== STDERR OUTPUT ===")); if (error && error.length > 0) { + // Split error into lines and add each one + const errorLines = error.split("\n"); + for (let i = 0; i < errorLines.length; i++) { + const line = errorLines[i].trim(); + if (line.length > 0) { + root.addDebugInfo(qsTr("STDERR: %1").arg(line)); + } + } + // Check for specific errors that indicate password is needed // Be careful not to match success messages const needsPassword = (error.includes("Secrets were required") || @@ -270,11 +530,19 @@ Singleton { const pending = root.pendingConnection; root.pendingConnection = null; pending.callback(); - } else if (error && error.length > 0 && !error.includes("Connection activated")) { - // Only log non-success messages - console.warn("Network connection error:", error); + } else if (error && error.length > 0 && !error.includes("Connection activated") && !error.includes("successfully")) { + // Log all errors (except success messages) + root.setConnectionStatus(qsTr("Error: %1").arg(errorLines[0] || error)); + // Emit signal for UI to handle + root.connectionFailed(root.pendingConnection ? root.pendingConnection.ssid : ""); + } else if (error && (error.includes("Connection activated") || error.includes("successfully"))) { + root.addDebugInfo(qsTr("Connection successful!")); + root.setConnectionStatus(qsTr("Connection successful!")); } + } else { + root.addDebugInfo(qsTr("STDERR: (empty)")); } + root.addDebugInfo(qsTr("====================")); } } } @@ -321,6 +589,26 @@ Singleton { } } + Process { + id: deleteConnectionProc + + // Delete connection profile - refresh network list and saved connections after deletion + onExited: { + // Refresh network list and saved connections after deletion + getNetworks.running = true; + listConnectionsProc.running = true; + } + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + // Log error but don't fail - connection might not exist + console.warn("Network connection delete error:", error); + } + } + } + } + Process { id: getNetworks -- cgit v1.2.3-freya From ffe14748a2cf5bc5710fe24d0ccae80b8437f35d Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 20:57:38 -0500 Subject: notifs/toasts: reverted all changes to notifications to c0ea060f --- modules/drawers/Panels.qml | 88 ------------- modules/lock/NotifDock.qml | 4 +- modules/lock/NotifGroup.qml | 2 +- modules/notifications/AppIconBadge.qml | 57 -------- modules/notifications/Content.qml | 58 +-------- modules/notifications/Notification.qml | 72 +++++++---- modules/notifications/NotificationToast.qml | 120 ----------------- modules/notifications/NotificationToasts.qml | 186 --------------------------- modules/notifications/Wrapper.qml | 2 - modules/utilities/toasts/ToastItem.qml | 9 +- services/Notifs.qml | 72 +---------- 11 files changed, 61 insertions(+), 609 deletions(-) delete mode 100644 modules/notifications/AppIconBadge.qml delete mode 100644 modules/notifications/NotificationToast.qml delete mode 100644 modules/notifications/NotificationToasts.qml diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 8b5a251..4ce1182 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -8,10 +8,6 @@ import qs.modules.bar.popouts as BarPopouts import qs.modules.utilities as Utilities import qs.modules.utilities.toasts as Toasts import qs.modules.sidebar as Sidebar -import qs.components -import qs.components.controls -import qs.components.effects -import qs.services import Quickshell import QtQuick @@ -31,7 +27,6 @@ Item { readonly property alias utilities: utilities readonly property alias toasts: toasts readonly property alias sidebar: sidebar - readonly property alias clearAllButton: clearAllButton anchors.fill: parent anchors.margins: Config.border.thickness @@ -59,89 +54,6 @@ Item { anchors.right: parent.right } - // Clear all notifications button - positioned to the left of the notification panel - Item { - id: clearAllButton - - readonly property bool hasNotifications: Notifs.notClosed.length > 0 - readonly property bool panelVisible: notifications.height > 0 || notifications.implicitHeight > 0 - readonly property bool shouldShow: hasNotifications && panelVisible - - anchors.top: notifications.top - anchors.right: notifications.left - anchors.rightMargin: Appearance.padding.normal - anchors.topMargin: Appearance.padding.large - - width: button.implicitWidth - height: button.implicitHeight - enabled: shouldShow - - IconButton { - id: button - - icon: "clear_all" - radius: Appearance.rounding.normal - padding: Appearance.padding.normal - font.pointSize: Math.round(Appearance.font.size.large * 1.2) - - onClicked: { - // Clear all notifications - for (const notif of Notifs.list.slice()) - notif.close(); - } - - Elevation { - anchors.fill: parent - radius: parent.radius - z: -1 - level: button.stateLayer.containsMouse ? 4 : 3 - } - } - - // Keep notification panel visible when hovering over the button - MouseArea { - anchors.fill: button - hoverEnabled: true - acceptedButtons: Qt.NoButton - onEntered: { - if (notifications.content && Notifs.notClosed.length > 0) { - notifications.content.show(); - } - } - onExited: { - // Panel will be hidden by Interactions.qml if mouse is not over panel or button - } - } - - Behavior on opacity { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on scale { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - opacity: shouldShow ? 1 : 0 - scale: shouldShow ? 1 : 0.5 - } - - Notifications.NotificationToasts { - id: notificationToasts - - panels: root - - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: Config.border.thickness - anchors.rightMargin: Config.border.thickness - } - Session.Wrapper { id: session diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index db087bd..7551e68 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -22,7 +22,7 @@ ColumnLayout { StyledText { Layout.fillWidth: true - text: Notifs.notClosed.length > 0 ? qsTr("%1 notification%2").arg(Notifs.notClosed.length).arg(Notifs.notClosed.length === 1 ? "" : "s") : qsTr("Notifications") + text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline font.family: Appearance.font.family.mono font.weight: 500 @@ -42,7 +42,7 @@ ColumnLayout { anchors.centerIn: parent asynchronous: true active: opacity > 0 - opacity: Notifs.notClosed.length > 0 ? 0 : 1 + opacity: Notifs.list.length > 0 ? 0 : 1 sourceComponent: ColumnLayout { spacing: Appearance.spacing.large diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 50b14ae..2a08c26 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -16,7 +16,7 @@ StyledRect { required property string modelData - readonly property list notifs: Notifs.notClosed.filter(notif => notif.appName === modelData) + readonly property list notifs: Notifs.list.filter(notif => notif.appName === modelData) readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? "" readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low" diff --git a/modules/notifications/AppIconBadge.qml b/modules/notifications/AppIconBadge.qml deleted file mode 100644 index 286522f..0000000 --- a/modules/notifications/AppIconBadge.qml +++ /dev/null @@ -1,57 +0,0 @@ -import qs.components -import qs.components.effects -import qs.services -import qs.config -import qs.utils -import Quickshell -import Quickshell.Services.Notifications -import QtQuick - -StyledRect { - id: root - - required property Notifs.Notif modelData - required property bool hasImage - required property bool hasAppIcon - - radius: Appearance.rounding.full - color: modelData.getBadgeBackgroundColor() - implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image - implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image - - Loader { - id: icon - - active: root.hasAppIcon - asynchronous: false - visible: active - - anchors.centerIn: parent - - width: Math.round(parent.width * 0.6) - height: Math.round(parent.width * 0.6) - - sourceComponent: ColouredIcon { - anchors.fill: parent - source: Quickshell.iconPath(root.modelData.appIcon) - colour: root.modelData.getIconColor() - layer.enabled: root.modelData.appIcon.endsWith("symbolic") - } - } - - Loader { - active: !root.hasAppIcon - asynchronous: false - visible: active - anchors.centerIn: parent - anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 - anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 - - sourceComponent: MaterialIcon { - text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) - color: root.modelData.getIconColor() - font.pointSize: Appearance.font.size.large - } - } -} - diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index 035a228..2d4590e 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -13,8 +13,6 @@ Item { required property Item panels readonly property int padding: Appearance.padding.large - property bool shouldShow: false - anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right @@ -22,16 +20,13 @@ Item { implicitWidth: Config.notifs.sizes.width + padding * 2 implicitHeight: { const count = list.count; - if (count === 0 || !shouldShow) + if (count === 0) return 0; let height = (count - 1) * Appearance.spacing.smaller; for (let i = 0; i < count; i++) height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; - const screenHeight = QsWindow.window?.screen?.height ?? 0; - const maxHeight = Math.floor(screenHeight * 0.45); - if (visibilities && panels) { if (visibilities.osd) { const h = panels.osd.y - Config.border.rounding * 2 - padding * 2; @@ -46,8 +41,7 @@ Item { } } - const availableHeight = Math.min(maxHeight, screenHeight - Config.border.thickness * 2); - return Math.min(availableHeight, height + padding * 2); + return Math.min((QsWindow.window?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); } ClippingWrapperRectangle { @@ -61,7 +55,7 @@ Item { id: list model: ScriptModel { - values: [...Notifs.notClosed] + values: Notifs.popups.filter(n => !n.closed) } anchors.fill: parent @@ -198,52 +192,6 @@ Item { } } - Timer { - id: hideTimer - - interval: 5000 - onTriggered: { - if (list.count > 0) - root.shouldShow = false; - } - } - - function show(): void { - if (list.count > 0) { - shouldShow = true; - hideTimer.restart(); - } - } - - Connections { - target: list - - function onCountChanged(): void { - if (list.count === 0) { - root.shouldShow = false; - hideTimer.stop(); - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.NoButton - onEntered: { - if (list.count > 0) { - root.shouldShow = true; - hideTimer.restart(); - } - } - onExited: { - if (list.count > 0) { - root.shouldShow = false; - hideTimer.stop(); - } - } - } - Behavior on implicitHeight { Anim {} } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index bc5c086..95507fc 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -17,31 +17,22 @@ StyledRect { required property Notifs.Notif modelData readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 - readonly property int nonAnimHeight: { - const baseHeight = summary.implicitHeight + inner.anchors.margins * 2; - return root.expanded - ? baseHeight + appName.height + body.height + actions.height + actions.anchors.topMargin - : baseHeight + bodyPreview.height; - } + readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 property bool expanded - property bool disableSlideIn: false - color: modelData.getBackgroundColor() + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.normal implicitWidth: Config.notifs.sizes.width implicitHeight: inner.implicitHeight - x: disableSlideIn ? 0 : Config.notifs.sizes.width + x: Config.notifs.sizes.width Component.onCompleted: { - if (!root.disableSlideIn) { - x = 0; - } + x = 0; modelData.lock(this); } Component.onDestruction: modelData.unlock(this) Behavior on x { - enabled: !disableSlideIn Anim { easing.bezierCurve: Appearance.anim.curves.emphasizedDecel } @@ -143,18 +134,53 @@ StyledRect { Loader { id: appIcon - active: !root.hasImage || root.hasAppIcon - asynchronous: false + active: root.hasAppIcon || !root.hasImage + asynchronous: true anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter anchors.right: root.hasImage ? image.right : undefined anchors.bottom: root.hasImage ? image.bottom : undefined - sourceComponent: AppIconBadge { - modelData: root.modelData - hasImage: root.hasImage - hasAppIcon: root.hasAppIcon + sourceComponent: StyledRect { + radius: Appearance.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer + implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + + Loader { + id: icon + + active: root.hasAppIcon + asynchronous: true + + anchors.centerIn: parent + + width: Math.round(parent.width * 0.6) + height: Math.round(parent.width * 0.6) + + sourceComponent: ColouredIcon { + anchors.fill: parent + source: Quickshell.iconPath(root.modelData.appIcon) + colour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + layer.enabled: root.modelData.appIcon.endsWith("symbolic") + } + } + + Loader { + active: !root.hasAppIcon + asynchronous: true + anchors.centerIn: parent + anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 + anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 + + sourceComponent: MaterialIcon { + text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) + + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + font.pointSize: Appearance.font.size.large + } + } } } @@ -296,7 +322,7 @@ StyledRect { StateLayer { radius: Appearance.rounding.full - color: root.modelData.getStateLayerColor() + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface function onClicked() { root.expanded = !root.expanded; @@ -417,7 +443,7 @@ StyledRect { required property var modelData radius: Appearance.rounding.full - color: root.modelData.getActionBackgroundColor() + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2 Layout.preferredHeight: actionText.height + Appearance.padding.small * 2 @@ -426,7 +452,7 @@ StyledRect { StateLayer { radius: Appearance.rounding.full - color: root.modelData.getStateLayerColor() + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface function onClicked(): void { action.modelData.invoke(); @@ -438,7 +464,7 @@ StyledRect { anchors.centerIn: parent text: actionTextMetrics.elidedText - color: root.modelData.getActionTextColor() + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } diff --git a/modules/notifications/NotificationToast.qml b/modules/notifications/NotificationToast.qml deleted file mode 100644 index 90414fe..0000000 --- a/modules/notifications/NotificationToast.qml +++ /dev/null @@ -1,120 +0,0 @@ -import qs.components -import qs.components.effects -import qs.services -import qs.config -import qs.utils -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Notifications -import QtQuick -import QtQuick.Layouts - -StyledRect { - id: root - - required property Notifs.Notif modelData - - readonly property bool hasImage: modelData.image.length > 0 - readonly property bool hasAppIcon: modelData.appIcon.length > 0 - - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2 - - radius: Appearance.rounding.normal - color: Colours.palette.m3surface - - border.width: 1 - border.color: Colours.palette.m3outlineVariant - - Elevation { - anchors.fill: parent - radius: parent.radius - opacity: parent.opacity - z: -1 - level: 3 - } - - RowLayout { - id: layout - - anchors.fill: parent - anchors.margins: Appearance.padding.smaller - anchors.leftMargin: Appearance.padding.normal - anchors.rightMargin: Appearance.padding.normal - spacing: Appearance.spacing.normal - - Item { - Layout.preferredWidth: Config.notifs.sizes.image - Layout.preferredHeight: Config.notifs.sizes.image - - Loader { - id: imageLoader - - active: root.hasImage - asynchronous: true - anchors.fill: parent - - sourceComponent: ClippingRectangle { - radius: Appearance.rounding.full - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image - - Image { - anchors.fill: parent - source: Qt.resolvedUrl(root.modelData.image) - fillMode: Image.PreserveAspectCrop - cache: false - asynchronous: true - } - } - } - - Loader { - id: appIconLoader - - active: root.hasAppIcon || !root.hasImage - asynchronous: true - - anchors.horizontalCenter: root.hasImage ? undefined : parent.horizontalCenter - anchors.verticalCenter: root.hasImage ? undefined : parent.verticalCenter - anchors.right: root.hasImage ? parent.right : undefined - anchors.bottom: root.hasImage ? parent.bottom : undefined - - sourceComponent: AppIconBadge { - modelData: root.modelData - hasImage: root.hasImage - hasAppIcon: root.hasAppIcon - } - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - - StyledText { - id: title - - Layout.fillWidth: true - text: root.modelData.summary - color: Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.normal - elide: Text.ElideRight - } - - StyledText { - Layout.fillWidth: true - textFormat: Text.StyledText - text: root.modelData.body - color: Colours.palette.m3onSurface - opacity: 0.8 - elide: Text.ElideRight - } - } - } - - Behavior on border.color { - CAnim {} - } -} diff --git a/modules/notifications/NotificationToasts.qml b/modules/notifications/NotificationToasts.qml deleted file mode 100644 index 96fe817..0000000 --- a/modules/notifications/NotificationToasts.qml +++ /dev/null @@ -1,186 +0,0 @@ -pragma ComponentBehavior: Bound - -import qs.components -import qs.config -import qs.services -import Quickshell -import Quickshell.Widgets -import QtQuick - -Item { - id: root - - required property Item panels - - readonly property int spacing: Appearance.spacing.small - readonly property int maxToasts: 5 - readonly property bool listVisible: panels.notifications.content.shouldShow - - property bool flag - property var activeToasts: new Set() - - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: Appearance.padding.normal - - implicitWidth: Config.notifs.sizes.width - implicitHeight: { - if (listVisible) - return 0; - - let height = -spacing; - for (let i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i) as ToastWrapper; - if (item && !item.modelData.closed && !item.previewHidden) - height += item.implicitHeight + spacing; - } - return height; - } - - opacity: listVisible ? 0 : 1 - visible: opacity > 0 - - Behavior on opacity { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - } - } - - Repeater { - id: repeater - - model: ScriptModel { - values: { - const toasts = []; - let visibleCount = 0; - - for (const notif of Notifs.list) { - if (notif.showAsToast) { - root.activeToasts.add(notif); - } - if (notif.closed) { - root.activeToasts.delete(notif); - } - } - - for (const notif of Notifs.list) { - if (root.activeToasts.has(notif)) { - toasts.push(notif); - if (notif.showAsToast && !notif.closed) { - visibleCount++; - if (visibleCount > root.maxToasts) - break; - } - } - } - return toasts; - } - onValuesChanged: root.flagChanged() - } - - ToastWrapper {} - } - - component ToastWrapper: MouseArea { - id: toast - - required property int index - required property Notifs.Notif modelData - - readonly property bool previewHidden: { - let extraHidden = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i); - if (item && item.modelData.closed) - extraHidden++; - } - return index >= root.maxToasts + extraHidden; - } - - opacity: modelData.closed || previewHidden || !modelData.showAsToast ? 0 : 1 - scale: modelData.closed || previewHidden || !modelData.showAsToast ? 0.7 : 1 - - anchors.topMargin: { - root.flag; - let margin = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i) as ToastWrapper; - if (item && !item.modelData.closed && !item.previewHidden) - margin += item.implicitHeight + root.spacing; - } - return margin; - } - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - implicitHeight: toastInner.implicitHeight - - acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton - onClicked: { - modelData.showAsToast = false; - modelData.close(); - } - - Component.onCompleted: modelData.lock(this) - - onPreviewHiddenChanged: { - if (initAnim.running && previewHidden) - initAnim.stop(); - } - - Anim { - id: initAnim - - Component.onCompleted: running = !toast.previewHidden - - target: toast - properties: "opacity,scale" - from: 0 - to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - - ParallelAnimation { - running: toast.modelData.closed || (!toast.modelData.showAsToast && !toast.modelData.closed) - onStarted: toast.anchors.topMargin = toast.anchors.topMargin - onFinished: { - if (toast.modelData.closed) - toast.modelData.unlock(toast); - } - - Anim { - target: toast - property: "opacity" - to: 0 - } - Anim { - target: toast - property: "scale" - to: 0.7 - } - } - - NotificationToast { - id: toastInner - - modelData: toast.modelData - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim {} - } - - Behavior on anchors.topMargin { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - } -} diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 4b54883..61acc56 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -8,8 +8,6 @@ Item { required property var visibilities required property Item panels - readonly property alias content: content - visible: height > 0 implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) implicitHeight: content.implicitHeight diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index 477a23c..f475500 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -28,13 +28,14 @@ StyledRect { border.width: 1 border.color: { + let colour = Colours.palette.m3outlineVariant; if (root.modelData.type === Toast.Success) - return Colours.palette.m3success; + colour = Colours.palette.m3success; if (root.modelData.type === Toast.Warning) - return Colours.palette.m3secondaryContainer; + colour = Colours.palette.m3secondaryContainer; if (root.modelData.type === Toast.Error) - return Colours.palette.m3error; - return Colours.palette.m3outlineVariant; + colour = Colours.palette.m3error; + return Qt.alpha(colour, 0.3); } Elevation { diff --git a/services/Notifs.qml b/services/Notifs.qml index 82ed8c4..4a89c7f 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -77,10 +77,8 @@ Singleton { onNotification: notif => { notif.tracked = true; - const shouldShowAsToast = !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar); const comp = notifComp.createObject(root, { - popup: shouldShowAsToast, - showAsToast: shouldShowAsToast, + popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), notification: notif }); root.list = [comp, ...root.list]; @@ -145,7 +143,6 @@ Singleton { property bool popup property bool closed - property bool showAsToast: false property var locks: new Set() property date time: new Date() @@ -179,74 +176,7 @@ Singleton { property bool hasActionIcons property list actions - readonly property bool isCritical: urgency === NotificationUrgency.Critical - readonly property bool isLow: urgency === NotificationUrgency.Low - - function getBackgroundColor(): color { - if (isCritical) return Colours.palette.m3secondaryContainer; - return Colours.tPalette.m3surfaceContainer; - } - - function getBadgeBackgroundColor(): color { - if (isCritical) return Colours.palette.m3error; - if (isLow) return Colours.layer(Colours.palette.m3surfaceContainerHighest, 2); - return Colours.palette.m3secondaryContainer; - } - - function getIconColor(): color { - if (isCritical) return Colours.palette.m3onError; - if (isLow) return Colours.palette.m3onSurface; - return Colours.palette.m3onSecondaryContainer; - } - - function getStateLayerColor(): color { - if (isCritical) return Colours.palette.m3onSecondaryContainer; - return Colours.palette.m3onSurface; - } - - function getActionBackgroundColor(): color { - if (isCritical) return Colours.palette.m3secondary; - return Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); - } - - function getActionTextColor(): color { - if (isCritical) return Colours.palette.m3onSecondary; - return Colours.palette.m3onSurfaceVariant; - } - readonly property Timer timer: Timer { - id: toastTimer - - running: notif.showAsToast - interval: { - let timeout = notif.expireTimeout; - if (timeout <= 0) { - switch (notif.urgency) { - case NotificationUrgency.Critical: - timeout = 10000; - break; - case NotificationUrgency.Normal: - timeout = 5000; - break; - case NotificationUrgency.Low: - timeout = 5000; - break; - default: - timeout = 5000; - } - } - return timeout; - } - onTriggered: { - if (Config.notifs.expire) - notif.popup = false; - if (notif.showAsToast) { - notif.showAsToast = false; - } - } - } - - readonly property Timer popupTimer: Timer { running: true interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout onTriggered: { -- cgit v1.2.3-freya From fc223237f0938c7904360d8c2674368bffa53373 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 21:58:14 -0500 Subject: controlcenter: wireless panel rewrite --- modules/controlcenter/Panes.qml | 2 +- modules/controlcenter/network/Details.qml | 213 --------- modules/controlcenter/network/NetworkList.qml | 228 --------- modules/controlcenter/network/NetworkPane.qml | 154 ------ modules/controlcenter/network/PasswordDialog.qml | 514 --------------------- modules/controlcenter/network/Settings.qml | 166 ------- modules/controlcenter/network/SimpleButton.qml | 36 ++ .../network/WirelessConnectionHelper.qml | 46 ++ modules/controlcenter/network/WirelessDetails.qml | 158 +++++++ modules/controlcenter/network/WirelessList.qml | 207 +++++++++ modules/controlcenter/network/WirelessPane.qml | 155 +++++++ .../network/WirelessPasswordDialog.qml | 299 ++++++++++++ modules/controlcenter/network/WirelessSettings.qml | 82 ++++ 13 files changed, 984 insertions(+), 1276 deletions(-) delete mode 100644 modules/controlcenter/network/Details.qml delete mode 100644 modules/controlcenter/network/NetworkList.qml delete mode 100644 modules/controlcenter/network/NetworkPane.qml delete mode 100644 modules/controlcenter/network/PasswordDialog.qml delete mode 100644 modules/controlcenter/network/Settings.qml create mode 100644 modules/controlcenter/network/SimpleButton.qml create mode 100644 modules/controlcenter/network/WirelessConnectionHelper.qml create mode 100644 modules/controlcenter/network/WirelessDetails.qml create mode 100644 modules/controlcenter/network/WirelessList.qml create mode 100644 modules/controlcenter/network/WirelessPane.qml create mode 100644 modules/controlcenter/network/WirelessPasswordDialog.qml create mode 100644 modules/controlcenter/network/WirelessSettings.qml diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 94bea9a..11e60d3 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -36,7 +36,7 @@ ClippingRectangle { Pane { index: 1 - sourceComponent: NetworkPane { + sourceComponent: WirelessPane { session: root.session } } diff --git a/modules/controlcenter/network/Details.qml b/modules/controlcenter/network/Details.qml deleted file mode 100644 index 5e636a2..0000000 --- a/modules/controlcenter/network/Details.qml +++ /dev/null @@ -1,213 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import qs.components -import qs.components.controls -import qs.components.effects -import qs.components.containers -import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts - -Item { - id: root - - required property Session session - readonly property var network: session.network.active - - Component.onCompleted: { - if (network && network.active) { - Network.updateWirelessDeviceDetails(); - } - } - - onNetworkChanged: { - if (network && network.active) { - Network.updateWirelessDeviceDetails(); - } else { - Network.wirelessDeviceDetails = null; - } - } - - Connections { - target: Network - function onActiveChanged() { - if (root.network && root.network.active && Network.active && Network.active.ssid === root.network.ssid) { - Network.updateWirelessDeviceDetails(); - } else if (!root.network || !root.network.active) { - Network.wirelessDeviceDetails = null; - } - } - } - - StyledFlickable { - anchors.fill: parent - - flickableDirection: Flickable.VerticalFlick - contentHeight: layout.height - - ColumnLayout { - id: layout - - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.normal - - ConnectionHeader { - icon: root.network?.isSecure ? "lock" : "wifi" - title: root.network?.ssid ?? qsTr("Unknown") - } - - SectionHeader { - title: qsTr("Connection status") - description: qsTr("Connection settings for this network") - } - - SectionContainer { - ToggleRow { - label: qsTr("Connected") - checked: root.network?.active ?? false - toggle.onToggled: { - if (checked) { - // If already connected to a different network, disconnect first - if (Network.active && Network.active.ssid !== root.network.ssid) { - Network.disconnectFromNetwork(); - // Wait a moment before connecting to new network - Qt.callLater(() => { - connectToNetwork(); - }); - } else { - connectToNetwork(); - } - } else { - Network.disconnectFromNetwork(); - } - } - - function connectToNetwork(): void { - if (root.network.isSecure) { - // Try connecting without password first (in case it's saved) - Network.connectToNetworkWithPasswordCheck( - root.network.ssid, - root.network.isSecure, - () => { - // Callback: connection failed, show password dialog - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = root.network; - }, - root.network.bssid - ); - } else { - Network.connectToNetwork(root.network.ssid, "", root.network.bssid, null); - } - } - } - - Button { - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal - visible: root.network && root.network.ssid && Network.savedConnections.includes(root.network.ssid) - color: Colours.palette.m3errorContainer - onColor: Colours.palette.m3onErrorContainer - text: qsTr("Forget Network") - - onClicked: { - if (root.network && root.network.ssid) { - // Disconnect first if connected - if (root.network.active) { - Network.disconnectFromNetwork(); - } - // Delete the connection profile - Network.forgetNetwork(root.network.ssid); - } - } - } - } - - SectionHeader { - title: qsTr("Network properties") - description: qsTr("Additional information") - } - - SectionContainer { - contentSpacing: Appearance.spacing.small / 2 - - PropertyRow { - label: qsTr("SSID") - value: root.network?.ssid ?? qsTr("Unknown") - } - - PropertyRow { - showTopMargin: true - label: qsTr("BSSID") - value: root.network?.bssid ?? qsTr("Unknown") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Signal strength") - value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Frequency") - value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Security") - value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A") - } - } - - SectionHeader { - title: qsTr("Connection information") - description: qsTr("Network connection details") - } - - SectionContainer { - ConnectionInfoSection { - deviceDetails: Network.wirelessDeviceDetails - } - } - - } - } - - component Button: StyledRect { - property color onColor: Colours.palette.m3onSurface - property alias disabled: stateLayer.disabled - property alias text: label.text - property alias enabled: stateLayer.enabled - - Layout.fillWidth: true - implicitHeight: label.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.normal - - StateLayer { - id: stateLayer - color: parent.onColor - function onClicked(): void { - if (parent.enabled !== false) { - parent.clicked(); - } - } - } - - StyledText { - id: label - anchors.centerIn: parent - color: parent.onColor - } - - signal clicked - } - -} - - - - diff --git a/modules/controlcenter/network/NetworkList.qml b/modules/controlcenter/network/NetworkList.qml deleted file mode 100644 index 6c4158c..0000000 --- a/modules/controlcenter/network/NetworkList.qml +++ /dev/null @@ -1,228 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import qs.components -import qs.components.controls -import qs.components.containers -import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts - -ColumnLayout { - id: root - - required property Session session - - spacing: Appearance.spacing.small - - RowLayout { - spacing: Appearance.spacing.smaller - - StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - ToggleButton { - toggled: Network.wifiEnabled - icon: "wifi" - accent: "Tertiary" - - onClicked: { - Network.toggleWifi(); - } - } - - ToggleButton { - toggled: Network.scanning - icon: "wifi_find" - accent: "Secondary" - - onClicked: { - Network.rescanWifi(); - } - } - - ToggleButton { - toggled: !root.session.network.active - icon: "settings" - accent: "Primary" - - onClicked: { - if (root.session.network.active) - root.session.network.active = null; - else { - root.session.network.active = view.model.get(0)?.modelData ?? null; - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Networks (%1)").arg(Network.networks.length) - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - - StyledText { - visible: Network.scanning - text: qsTr("Scanning...") - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.small - } - } - - StyledText { - text: qsTr("All available WiFi networks") - color: Colours.palette.m3outline - } - - StyledListView { - id: view - - Layout.fillWidth: true - Layout.fillHeight: true - - model: Network.networks - - spacing: Appearance.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: view - } - - delegate: StyledRect { - required property var modelData - - anchors.left: parent.left - anchors.right: parent.right - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.network.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: root.session.network.active === modelData ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - root.session.network.active = modelData; - } - } - - RowLayout { - id: rowLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 - - radius: Appearance.rounding.normal - color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - - MaterialIcon { - id: icon - - anchors.centerIn: parent - text: modelData.isSecure ? "lock" : "wifi" - font.pointSize: Appearance.font.size.large - fill: modelData.active ? 1 : 0 - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - } - - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - - text: modelData.ssid || qsTr("Unknown") - } - - StyledText { - text: modelData.active ? qsTr("Connected") : (modelData.isSecure ? qsTr("Secured") : qsTr("Open")) - color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: modelData.active ? 500 : 400 - } - - StyledText { - text: qsTr("%1%").arg(modelData.strength) - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 - - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) - - StateLayer { - function onClicked(): void { - if (modelData.active) { - Network.disconnectFromNetwork(); - } else { - // If already connected to a different network, disconnect first - if (Network.active && Network.active.ssid !== modelData.ssid) { - Network.disconnectFromNetwork(); - // Wait a moment before connecting to new network - Qt.callLater(() => { - connectToNetwork(); - }); - } else { - connectToNetwork(); - } - } - } - - function connectToNetwork(): void { - if (modelData.isSecure) { - // Try connecting without password first (in case it's saved) - Network.connectToNetworkWithPasswordCheck( - modelData.ssid, - modelData.isSecure, - () => { - // Callback: connection failed, show password dialog - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = modelData; - }, - modelData.bssid - ); - } else { - Network.connectToNetwork(modelData.ssid, "", modelData.bssid, null); - } - } - } - - MaterialIcon { - id: connectIcon - - anchors.centerIn: parent - text: modelData.active ? "link_off" : "link" - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - } - } - - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 - } - } -} diff --git a/modules/controlcenter/network/NetworkPane.qml b/modules/controlcenter/network/NetworkPane.qml deleted file mode 100644 index fe18274..0000000 --- a/modules/controlcenter/network/NetworkPane.qml +++ /dev/null @@ -1,154 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import qs.components -import qs.components.effects -import qs.components.containers -import qs.config -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts - -RowLayout { - id: root - - required property Session session - - anchors.fill: parent - - spacing: 0 - - Item { - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - NetworkList { - 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 - - session: root.session - } - - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - 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 var pane: root.session.network.active - property string paneId: pane ? (pane.ssid || pane.bssid || "") : "" - - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - - opacity: 1 - scale: 1 - transformOrigin: Item.Center - - clip: false - asynchronous: true - sourceComponent: pane ? details : settings - - 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 - } - } - PropertyAction {} - 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 - } - } - } - } - - onPaneChanged: { - paneId = pane ? (pane.ssid || pane.bssid || "") : ""; - } - } - } - - InnerBorder { - id: rightBorder - - leftThickness: Appearance.padding.normal / 2 - } - - Component { - id: settings - - StyledFlickable { - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - - Settings { - id: settingsInner - - anchors.left: parent.left - anchors.right: parent.right - session: root.session - } - } - } - - Component { - id: details - - Details { - session: root.session - } - } - } - - PasswordDialog { - anchors.fill: parent - session: root.session - z: 1000 - } - - component Anim: NumberAnimation { - target: loader - duration: Appearance.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - } -} diff --git a/modules/controlcenter/network/PasswordDialog.qml b/modules/controlcenter/network/PasswordDialog.qml deleted file mode 100644 index 7aa698b..0000000 --- a/modules/controlcenter/network/PasswordDialog.qml +++ /dev/null @@ -1,514 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import qs.components -import qs.components.controls -import qs.components.effects -import qs.components.containers -import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts - -Item { - id: root - - required property Session session - readonly property var network: { - // Try pendingNetwork first, then fall back to active network selection - if (session.network.pendingNetwork) { - return session.network.pendingNetwork; - } - // Fallback to active network if available - if (session.network.active) { - return session.network.active; - } - return null; - } - - visible: session.network.showPasswordDialog - enabled: visible - focus: visible - - // Ensure network is set when dialog opens - Component.onCompleted: { - if (visible && !session.network.pendingNetwork && session.network.active) { - session.network.pendingNetwork = session.network.active; - } - } - - Connections { - target: root - function onVisibleChanged(): void { - if (visible && !session.network.pendingNetwork && session.network.active) { - session.network.pendingNetwork = session.network.active; - } - } - } - - Keys.onEscapePressed: { - root.session.network.showPasswordDialog = false; - passwordField.text = ""; - } - - Rectangle { - anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.5) - opacity: root.visible ? 1 : 0 - - Behavior on opacity { - NumberAnimation { - duration: 200 - } - } - - MouseArea { - anchors.fill: parent - onClicked: { - root.session.network.showPasswordDialog = false; - passwordField.text = ""; - } - } - } - - StyledRect { - id: dialog - - anchors.centerIn: parent - - implicitWidth: 400 - implicitHeight: content.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surface - opacity: root.visible ? 1 : 0 - scale: root.visible ? 1 : 0.9 - - Behavior on opacity { - NumberAnimation { - duration: 200 - } - } - - Behavior on scale { - NumberAnimation { - duration: 200 - } - } - - Keys.onEscapePressed: { - root.session.network.showPasswordDialog = false; - passwordField.text = ""; - } - - ColumnLayout { - id: content - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.normal - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "lock" - font.pointSize: Appearance.font.size.extraLarge * 2 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Enter password") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : "" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - id: statusText - - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.spacing.small - visible: Network.connectionStatus.length > 0 || connectButton.connecting - text: { - if (Network.connectionStatus.length > 0) { - return Network.connectionStatus; - } else if (connectButton.connecting) { - return qsTr("Connecting..."); - } - return ""; - } - color: { - const status = Network.connectionStatus; - if (status.includes("Error") || status.includes("error") || status.includes("failed")) { - return Colours.palette.m3error; - } else if (status.includes("successful") || status.includes("Connected") || status.includes("success")) { - return Colours.palette.m3primary; - } - return Colours.palette.m3onSurfaceVariant; - } - font.pointSize: Appearance.font.size.small - font.weight: (Network.connectionStatus.includes("Error") || Network.connectionStatus.includes("error")) ? 500 : 400 - wrapMode: Text.WordWrap - Layout.maximumWidth: parent.width - Appearance.padding.large * 2 - } - - Item { - Layout.topMargin: Appearance.spacing.large - Layout.fillWidth: true - implicitHeight: passwordField.implicitHeight + Appearance.padding.normal * 2 - - StyledRect { - anchors.fill: parent - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - border.width: passwordField.activeFocus ? 2 : 1 - border.color: passwordField.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline - - Behavior on border.color { - CAnim {} - } - } - - StyledTextField { - id: passwordField - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - echoMode: TextField.Password - placeholderText: qsTr("Password") - - Component.onCompleted: { - if (root.visible) { - forceActiveFocus(); - } - } - - Connections { - target: root - function onVisibleChanged(): void { - if (root.visible) { - passwordField.forceActiveFocus(); - passwordField.text = ""; - Network.connectionStatus = ""; - } - } - } - - Connections { - target: Network - function onConnectionStatusChanged(): void { - // Status updated, ensure it's visible - } - } - - Keys.onReturnPressed: { - if (connectButton.enabled) { - connectButton.clicked(); - } - } - Keys.onEnterPressed: { - if (connectButton.enabled) { - connectButton.clicked(); - } - } - } - } - - RowLayout { - Layout.topMargin: Appearance.spacing.normal - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - Button { - id: cancelButton - - Layout.fillWidth: true - color: Colours.palette.m3secondaryContainer - onColor: Colours.palette.m3onSecondaryContainer - text: qsTr("Cancel") - - function onClicked(): void { - root.session.network.showPasswordDialog = false; - passwordField.text = ""; - } - } - - Button { - id: connectButton - - Layout.fillWidth: true - color: Colours.palette.m3primary - onColor: Colours.palette.m3onPrimary - text: qsTr("Connect") - enabled: passwordField.text.length > 0 - - property bool connecting: false - - function onClicked(): void { - Network.connectionStatus = ""; - - // Get password first - const password = passwordField.text; - - // Try multiple ways to get the network - let networkToUse = null; - - // Try 1: root.network (computed property) - if (root.network) { - networkToUse = root.network; - } - - // Try 2: pendingNetwork - if (!networkToUse && root.session.network.pendingNetwork) { - networkToUse = root.session.network.pendingNetwork; - } - - // Try 3: active network - if (!networkToUse && root.session.network.active) { - networkToUse = root.session.network.active; - root.session.network.pendingNetwork = networkToUse; - } - - // Check all conditions - const hasNetwork = !!networkToUse; - const hasPassword = password && password.length > 0; - const notConnecting = !connecting; - - if (hasNetwork && hasPassword && notConnecting) { - // Set status immediately - Network.connectionStatus = qsTr("Preparing to connect..."); - - // Keep dialog open and track connection - connecting = true; - connectButton.enabled = false; - connectButton.text = qsTr("Connecting..."); - - // Force immediate UI update - statusText.visible = true; - - // Store target SSID for later comparison - const ssidToConnect = networkToUse.ssid || ""; - const bssidToConnect = networkToUse.bssid || ""; - - // Store the SSID we're connecting to so we can compare later - // even if root.network changes - content.connectingToSsid = ssidToConnect; - - // Execute connection immediately - Network.connectToNetwork( - ssidToConnect, - password, - bssidToConnect, - () => { - // Callback if connection fails - keep dialog open - connecting = false; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - content.connectingToSsid = ""; // Clear on failure - } - ); - - // Start connection check timer immediately - connectionCheckTimer.checkCount = 0; - connectionCheckTimer.start(); - - // Also check immediately after a short delay to catch quick connections - Qt.callLater(() => { - if (root.visible) { - closeDialogIfConnected(); - } - }); - } else { - // Show error in status - Network.connectionStatus = qsTr("Error: Cannot connect - missing network or password"); - } - } - } - } - - // Store the SSID we're connecting to when connection starts - property string connectingToSsid: "" - - property string targetSsid: { - // Track the SSID we're trying to connect to - // Prefer explicitly stored connectingToSsid, then computed values - if (connectingToSsid && connectingToSsid.length > 0) { - return connectingToSsid; - } - if (root.network && root.network.ssid) { - return root.network.ssid; - } - if (root.session.network.pendingNetwork && root.session.network.pendingNetwork.ssid) { - return root.session.network.pendingNetwork.ssid; - } - return ""; - } - - function closeDialogIfConnected(): bool { - // Check if we're connected to the network we're trying to connect to - const ssid = targetSsid; - - if (!ssid || ssid.length === 0) { - return false; - } - - if (!Network.active) { - return false; - } - - const activeSsid = Network.active.ssid || ""; - - if (activeSsid === ssid) { - // Connection succeeded - close dialog - connectionCheckTimer.stop(); - aggressiveCheckTimer.stop(); - connectionCheckTimer.checkCount = 0; - connectButton.connecting = false; - Network.connectionStatus = ""; - root.session.network.showPasswordDialog = false; - passwordField.text = ""; - content.connectingToSsid = ""; // Clear stored SSID - return true; - } - return false; - } - - Timer { - id: connectionCheckTimer - interval: 1000 // Check every 1 second for faster response - repeat: true - triggeredOnStart: false - property int checkCount: 0 - - onTriggered: { - checkCount++; - - // Try to close dialog if connected - const closed = content.closeDialogIfConnected(); - if (closed) { - return; - } - - if (connectButton.connecting) { - // Still connecting, check again - // Limit to 20 checks (20 seconds total) - if (checkCount >= 20) { - connectionCheckTimer.stop(); - connectionCheckTimer.checkCount = 0; - connectButton.connecting = false; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - } - } else { - // Not connecting anymore, stop timer - connectionCheckTimer.stop(); - connectionCheckTimer.checkCount = 0; - } - } - } - - Connections { - target: Network - function onActiveChanged(): void { - // Check immediately when active network changes - if (root.visible) { - // Check immediately - if connected, close right away - if (content.closeDialogIfConnected()) { - return; - } - - // Also check after a delay in case the active network isn't fully updated yet - Qt.callLater(() => { - if (root.visible) { - content.closeDialogIfConnected(); - } - }); - } - } - } - - // Also check when dialog becomes visible - Connections { - target: root - function onVisibleChanged(): void { - if (root.visible) { - // Check immediately when dialog opens - Qt.callLater(() => { - if (root.visible) { - closeDialogIfConnected(); - } - }); - } - } - } - - // Aggressive polling timer - checks every 500ms when dialog is visible and connecting - // This ensures we catch the connection even if signals are missed - Timer { - id: aggressiveCheckTimer - interval: 500 - repeat: true - running: root.visible && connectButton.connecting - triggeredOnStart: true - - onTriggered: { - if (root.visible && connectButton.connecting) { - if (content.closeDialogIfConnected()) { - stop(); - } - } else { - stop(); - } - } - } - } - } - - component Button: StyledRect { - property color onColor: Colours.palette.m3onSurface - property alias disabled: stateLayer.disabled - property alias text: label.text - property alias enabled: stateLayer.enabled - - function onClicked(): void { - } - - radius: Appearance.rounding.small - implicitHeight: label.implicitHeight + Appearance.padding.small * 2 - opacity: enabled ? 1 : 0.5 - - StateLayer { - id: stateLayer - - enabled: parent.enabled - color: parent.onColor - - function onClicked(): void { - if (enabled) { - parent.onClicked(); - } - } - } - - StyledText { - id: label - - anchors.centerIn: parent - animate: true - color: parent.onColor - font.pointSize: Appearance.font.size.normal - } - } -} - diff --git a/modules/controlcenter/network/Settings.qml b/modules/controlcenter/network/Settings.qml deleted file mode 100644 index 0794e6a..0000000 --- a/modules/controlcenter/network/Settings.qml +++ /dev/null @@ -1,166 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import qs.components -import qs.components.controls -import qs.components.effects -import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts - -ColumnLayout { - id: root - - required property Session session - - spacing: Appearance.spacing.normal - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "wifi" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Network settings") - font.pointSize: Appearance.font.size.large - font.bold: true - } - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("WiFi status") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("General WiFi settings") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: wifiStatus.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: wifiStatus - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.larger - - Toggle { - label: qsTr("WiFi enabled") - checked: Network.wifiEnabled - toggle.onToggled: { - Network.enableWifi(checked); - } - } - } - } - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Network information") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("Current network connection") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: networkInfo.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: networkInfo - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.small / 2 - - StyledText { - text: qsTr("Connected network") - } - - StyledText { - text: Network.active ? Network.active.ssid : qsTr("Not connected") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Signal strength") - } - - StyledText { - text: Network.active ? qsTr("%1%").arg(Network.active.strength) : qsTr("N/A") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Security") - } - - StyledText { - text: Network.active ? (Network.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Frequency") - } - - StyledText { - text: Network.active ? qsTr("%1 MHz").arg(Network.active.frequency) : qsTr("N/A") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - } - } - - component Toggle: RowLayout { - required property string label - property alias checked: toggle.checked - property alias toggle: toggle - - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: parent.label - } - - StyledSwitch { - id: toggle - - cLayer: 2 - } - } -} - diff --git a/modules/controlcenter/network/SimpleButton.qml b/modules/controlcenter/network/SimpleButton.qml new file mode 100644 index 0000000..49ab2f4 --- /dev/null +++ b/modules/controlcenter/network/SimpleButton.qml @@ -0,0 +1,36 @@ +import qs.components +import qs.components.effects +import qs.config +import QtQuick + +StyledRect { + id: root + + property color onColor: Colours.palette.m3onSurface + property alias disabled: stateLayer.disabled + property alias text: label.text + property alias enabled: stateLayer.enabled + + implicitWidth: label.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: label.implicitHeight + Appearance.padding.normal * 2 + radius: Appearance.rounding.normal + + StateLayer { + id: stateLayer + color: parent.onColor + function onClicked(): void { + if (parent.enabled !== false) { + parent.clicked(); + } + } + } + + StyledText { + id: label + anchors.centerIn: parent + color: parent.onColor + } + + signal clicked +} + diff --git a/modules/controlcenter/network/WirelessConnectionHelper.qml b/modules/controlcenter/network/WirelessConnectionHelper.qml new file mode 100644 index 0000000..d63b359 --- /dev/null +++ b/modules/controlcenter/network/WirelessConnectionHelper.qml @@ -0,0 +1,46 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import QtQuick + +QtObject { + id: root + + required property Session session + + function connectToNetwork(network: var): void { + if (!network) { + return; + } + + // If already connected to a different network, disconnect first + if (Network.active && Network.active.ssid !== network.ssid) { + Network.disconnectFromNetwork(); + Qt.callLater(() => { + performConnect(network); + }); + } else { + performConnect(network); + } + } + + function performConnect(network: var): void { + if (network.isSecure) { + // Try connecting without password first (in case it's saved) + Network.connectToNetworkWithPasswordCheck( + network.ssid, + network.isSecure, + () => { + // Callback: connection failed, show password dialog + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = network; + }, + network.bssid + ); + } else { + Network.connectToNetwork(network.ssid, "", network.bssid, null); + } + } +} + diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml new file mode 100644 index 0000000..e858fbc --- /dev/null +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -0,0 +1,158 @@ +pragma ComponentBehavior: Bound + +import ".." +import "." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Session session + readonly property var network: session.network.active + + readonly property var connectionHelper: WirelessConnectionHelper { + session: root.session + } + + Component.onCompleted: { + if (network && network.active) { + Network.updateWirelessDeviceDetails(); + } + } + + onNetworkChanged: { + if (network && network.active) { + Network.updateWirelessDeviceDetails(); + } else { + Network.wirelessDeviceDetails = null; + } + } + + Connections { + target: Network + function onActiveChanged() { + if (root.network && root.network.active && Network.active && Network.active.ssid === root.network.ssid) { + Network.updateWirelessDeviceDetails(); + } else if (!root.network || !root.network.active) { + Network.wirelessDeviceDetails = null; + } + } + } + + StyledFlickable { + anchors.fill: parent + + flickableDirection: Flickable.VerticalFlick + contentHeight: layout.height + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + + ConnectionHeader { + icon: root.network?.isSecure ? "lock" : "wifi" + title: root.network?.ssid ?? qsTr("Unknown") + } + + SectionHeader { + title: qsTr("Connection status") + description: qsTr("Connection settings for this network") + } + + SectionContainer { + ToggleRow { + label: qsTr("Connected") + checked: root.network?.active ?? false + toggle.onToggled: { + if (checked) { + root.connectionHelper.connectToNetwork(root.network); + } else { + Network.disconnectFromNetwork(); + } + } + } + + SimpleButton { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + visible: root.network && root.network.ssid && Network.savedConnections.includes(root.network.ssid) + color: Colours.palette.m3errorContainer + onColor: Colours.palette.m3onErrorContainer + text: qsTr("Forget Network") + + onClicked: { + if (root.network && root.network.ssid) { + // Disconnect first if connected + if (root.network.active) { + Network.disconnectFromNetwork(); + } + // Delete the connection profile + Network.forgetNetwork(root.network.ssid); + } + } + } + } + + SectionHeader { + title: qsTr("Network properties") + description: qsTr("Additional information") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("SSID") + value: root.network?.ssid ?? qsTr("Unknown") + } + + PropertyRow { + showTopMargin: true + label: qsTr("BSSID") + value: root.network?.bssid ?? qsTr("Unknown") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Signal strength") + value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Frequency") + value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Security") + value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A") + } + } + + SectionHeader { + title: qsTr("Connection information") + description: qsTr("Network connection details") + } + + SectionContainer { + ConnectionInfoSection { + deviceDetails: Network.wirelessDeviceDetails + } + } + + } + } +} + diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml new file mode 100644 index 0000000..1a4ba00 --- /dev/null +++ b/modules/controlcenter/network/WirelessList.qml @@ -0,0 +1,207 @@ +pragma ComponentBehavior: Bound + +import ".." +import "." +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + readonly property var connectionHelper: WirelessConnectionHelper { + session: root.session + } + + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + ToggleButton { + toggled: Network.wifiEnabled + icon: "wifi" + accent: "Tertiary" + + onClicked: { + Network.toggleWifi(); + } + } + + ToggleButton { + toggled: Network.scanning + icon: "wifi_find" + accent: "Secondary" + + onClicked: { + Network.rescanWifi(); + } + } + + ToggleButton { + toggled: !root.session.network.active + icon: "settings" + accent: "Primary" + + onClicked: { + if (root.session.network.active) + root.session.network.active = null; + else { + root.session.network.active = view.model.get(0)?.modelData ?? null; + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Networks (%1)").arg(Network.networks.length) + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + visible: Network.scanning + text: qsTr("Scanning...") + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.small + } + } + + StyledText { + text: qsTr("All available WiFi networks") + color: Colours.palette.m3outline + } + + StyledListView { + id: view + + Layout.fillWidth: true + Layout.fillHeight: true + + model: Network.networks + + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: view + } + + delegate: StyledRect { + required property var modelData + + anchors.left: parent.left + anchors.right: parent.right + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.network.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: root.session.network.active === modelData ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + root.session.network.active = modelData; + } + } + + RowLayout { + id: rowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: modelData.isSecure ? "lock" : "wifi" + font.pointSize: Appearance.font.size.large + fill: modelData.active ? 1 : 0 + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + + text: modelData.ssid || qsTr("Unknown") + } + + StyledText { + text: modelData.active ? qsTr("Connected") : (modelData.isSecure ? qsTr("Secured") : qsTr("Open")) + color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.active ? 500 : 400 + } + + StyledText { + text: qsTr("%1%").arg(modelData.strength) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) + + StateLayer { + function onClicked(): void { + if (modelData.active) { + Network.disconnectFromNetwork(); + } else { + root.connectionHelper.connectToNetwork(modelData); + } + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + text: modelData.active ? "link_off" : "link" + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + } + + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } +} + diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml new file mode 100644 index 0000000..a23b6e8 --- /dev/null +++ b/modules/controlcenter/network/WirelessPane.qml @@ -0,0 +1,155 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.effects +import qs.components.containers +import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property Session session + + anchors.fill: parent + + spacing: 0 + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true + + WirelessList { + 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 + + session: root.session + } + + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ClippingRectangle { + 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 var pane: root.session.network.active + property string paneId: pane ? (pane.ssid || pane.bssid || "") : "" + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + + clip: false + asynchronous: true + sourceComponent: pane ? details : settings + + 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 + } + } + PropertyAction {} + 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 + } + } + } + } + + onPaneChanged: { + paneId = pane ? (pane.ssid || pane.bssid || "") : ""; + } + } + } + + InnerBorder { + id: rightBorder + + leftThickness: Appearance.padding.normal / 2 + } + + Component { + id: settings + + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + + WirelessSettings { + id: settingsInner + + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } + } + } + + Component { + id: details + + WirelessDetails { + session: root.session + } + } + } + + WirelessPasswordDialog { + anchors.fill: parent + session: root.session + z: 1000 + } + + component Anim: NumberAnimation { + target: loader + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + } +} + diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml new file mode 100644 index 0000000..778fb4f --- /dev/null +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -0,0 +1,299 @@ +pragma ComponentBehavior: Bound + +import ".." +import "." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Session session + + readonly property var network: { + // Prefer pendingNetwork, then active network + if (session.network.pendingNetwork) { + return session.network.pendingNetwork; + } + if (session.network.active) { + return session.network.active; + } + return null; + } + + visible: session.network.showPasswordDialog + enabled: visible + focus: visible + + Keys.onEscapePressed: { + closeDialog(); + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.5) + opacity: root.visible ? 1 : 0 + + Behavior on opacity { + NumberAnimation { duration: 200 } + } + + MouseArea { + anchors.fill: parent + onClicked: closeDialog(); + } + } + + StyledRect { + id: dialog + + anchors.centerIn: parent + + implicitWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surface + opacity: root.visible ? 1 : 0 + scale: root.visible ? 1 : 0.9 + + Behavior on opacity { + NumberAnimation { duration: 200 } + } + + Behavior on scale { + NumberAnimation { duration: 200 } + } + + Keys.onEscapePressed: closeDialog(); + + ColumnLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "lock" + font.pointSize: Appearance.font.size.extraLarge * 2 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enter password") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + id: statusText + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.spacing.small + visible: Network.connectionStatus.length > 0 || connectButton.connecting + text: { + if (Network.connectionStatus.length > 0) { + return Network.connectionStatus; + } else if (connectButton.connecting) { + return qsTr("Connecting..."); + } + return ""; + } + color: { + const status = Network.connectionStatus; + if (status.includes("Error") || status.includes("error") || status.includes("failed")) { + return Colours.palette.m3error; + } else if (status.includes("successful") || status.includes("Connected") || status.includes("success")) { + return Colours.palette.m3primary; + } + return Colours.palette.m3onSurfaceVariant; + } + font.pointSize: Appearance.font.size.small + font.weight: (Network.connectionStatus.includes("Error") || Network.connectionStatus.includes("error")) ? 500 : 400 + wrapMode: Text.WordWrap + Layout.maximumWidth: parent.width - Appearance.padding.large * 2 + } + + Item { + Layout.topMargin: Appearance.spacing.large + Layout.fillWidth: true + implicitHeight: passwordField.implicitHeight + Appearance.padding.normal * 2 + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + border.width: passwordField.activeFocus ? 2 : 1 + border.color: passwordField.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline + + Behavior on border.color { + CAnim {} + } + } + + StyledTextField { + id: passwordField + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + echoMode: TextField.Password + placeholderText: qsTr("Password") + + Connections { + target: root + function onVisibleChanged(): void { + if (root.visible) { + passwordField.forceActiveFocus(); + passwordField.text = ""; + Network.clearConnectionStatus(); + } + } + } + + Keys.onReturnPressed: { + if (connectButton.enabled) { + connectButton.clicked(); + } + } + Keys.onEnterPressed: { + if (connectButton.enabled) { + connectButton.clicked(); + } + } + } + } + + RowLayout { + Layout.topMargin: Appearance.spacing.normal + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + SimpleButton { + id: cancelButton + + Layout.fillWidth: true + color: Colours.palette.m3secondaryContainer + onColor: Colours.palette.m3onSecondaryContainer + text: qsTr("Cancel") + + onClicked: closeDialog(); + } + + SimpleButton { + id: connectButton + + Layout.fillWidth: true + color: Colours.palette.m3primary + onColor: Colours.palette.m3onPrimary + text: qsTr("Connect") + enabled: passwordField.text.length > 0 && !connecting + + property bool connecting: false + + onClicked: { + if (!root.network || connecting) { + return; + } + + const password = passwordField.text; + if (!password || password.length === 0) { + return; + } + + // Set connecting state + connecting = true; + enabled = false; + text = qsTr("Connecting..."); + Network.clearConnectionStatus(); + + // Connect to network + Network.connectToNetwork( + root.network.ssid, + password, + root.network.bssid || "", + null + ); + + // Start monitoring connection + connectionMonitor.start(); + } + } + } + } + } + + Timer { + id: connectionMonitor + interval: 1000 + repeat: true + triggeredOnStart: false + + onTriggered: { + // Check if we're connected to the target network + if (root.network && Network.active && Network.active.ssid === root.network.ssid) { + // Successfully connected + stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + closeDialog(); + } else if (connectButton.connecting) { + // Still connecting, check status + const status = Network.connectionStatus; + if (status.includes("Error") || status.includes("error") || status.includes("failed")) { + // Connection failed + stop(); + connectButton.connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + } + } else { + // Not connecting anymore + stop(); + } + } + } + + Connections { + target: Network + function onActiveChanged() { + if (root.visible && root.network && Network.active && Network.active.ssid === root.network.ssid) { + // Connected successfully + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + closeDialog(); + } + } + } + + function closeDialog(): void { + session.network.showPasswordDialog = false; + passwordField.text = ""; + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); + Network.clearConnectionStatus(); + } +} + diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml new file mode 100644 index 0000000..7890099 --- /dev/null +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -0,0 +1,82 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "wifi" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Network settings") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("WiFi status") + description: qsTr("General WiFi settings") + } + + SectionContainer { + ToggleRow { + label: qsTr("WiFi enabled") + checked: Network.wifiEnabled + toggle.onToggled: { + Network.enableWifi(checked); + } + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Network information") + description: qsTr("Current network connection") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Connected network") + value: Network.active ? Network.active.ssid : qsTr("Not connected") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Signal strength") + value: Network.active ? qsTr("%1%").arg(Network.active.strength) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Security") + value: Network.active ? (Network.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Frequency") + value: Network.active ? qsTr("%1 MHz").arg(Network.active.frequency) : qsTr("N/A") + } + } +} + -- cgit v1.2.3-freya From b62a22b13d6d0c9704b58cf9d79ee89d4b156a74 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 22:09:51 -0500 Subject: controlcenter: wireless panel refactoring --- .../network/WirelessConnectionHelper.qml | 46 -------------- modules/controlcenter/network/WirelessDetails.qml | 71 ++++++++++++++++------ modules/controlcenter/network/WirelessList.qml | 46 ++++++++++++-- .../network/WirelessPasswordDialog.qml | 56 +++++++++-------- 4 files changed, 123 insertions(+), 96 deletions(-) delete mode 100644 modules/controlcenter/network/WirelessConnectionHelper.qml diff --git a/modules/controlcenter/network/WirelessConnectionHelper.qml b/modules/controlcenter/network/WirelessConnectionHelper.qml deleted file mode 100644 index d63b359..0000000 --- a/modules/controlcenter/network/WirelessConnectionHelper.qml +++ /dev/null @@ -1,46 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import qs.services -import QtQuick - -QtObject { - id: root - - required property Session session - - function connectToNetwork(network: var): void { - if (!network) { - return; - } - - // If already connected to a different network, disconnect first - if (Network.active && Network.active.ssid !== network.ssid) { - Network.disconnectFromNetwork(); - Qt.callLater(() => { - performConnect(network); - }); - } else { - performConnect(network); - } - } - - function performConnect(network: var): void { - if (network.isSecure) { - // Try connecting without password first (in case it's saved) - Network.connectToNetworkWithPasswordCheck( - network.ssid, - network.isSecure, - () => { - // Callback: connection failed, show password dialog - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = network; - }, - network.bssid - ); - } else { - Network.connectToNetwork(network.ssid, "", network.bssid, null); - } - } -} - diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index e858fbc..418c463 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -16,33 +16,28 @@ Item { required property Session session readonly property var network: session.network.active - - readonly property var connectionHelper: WirelessConnectionHelper { - session: root.session - } Component.onCompleted: { - if (network && network.active) { - Network.updateWirelessDeviceDetails(); - } + updateDeviceDetails(); } onNetworkChanged: { - if (network && network.active) { - Network.updateWirelessDeviceDetails(); - } else { - Network.wirelessDeviceDetails = null; - } + updateDeviceDetails(); } Connections { target: Network function onActiveChanged() { - if (root.network && root.network.active && Network.active && Network.active.ssid === root.network.ssid) { - Network.updateWirelessDeviceDetails(); - } else if (!root.network || !root.network.active) { - Network.wirelessDeviceDetails = null; - } + updateDeviceDetails(); + } + } + + function updateDeviceDetails(): void { + // Only update details if the selected network is currently active + if (network && Network.active && Network.active.ssid === network.ssid) { + Network.updateWirelessDeviceDetails(); + } else { + Network.wirelessDeviceDetails = null; } } @@ -75,7 +70,7 @@ Item { checked: root.network?.active ?? false toggle.onToggled: { if (checked) { - root.connectionHelper.connectToNetwork(root.network); + handleConnect(); } else { Network.disconnectFromNetwork(); } @@ -154,5 +149,45 @@ Item { } } + + function handleConnect(): void { + // If already connected to a different network, disconnect first + if (Network.active && Network.active.ssid !== root.network.ssid) { + Network.disconnectFromNetwork(); + Qt.callLater(() => { + connectToNetwork(); + }); + } else { + connectToNetwork(); + } + } + + function connectToNetwork(): void { + if (root.network.isSecure) { + // Check if we have a saved connection profile for this network + const hasSavedProfile = Network.savedConnections.includes(root.network.ssid); + + if (hasSavedProfile) { + // Try connecting with saved password - don't show dialog if it fails + // The saved password should work, but if connection fails for other reasons, + // we'll let the user try manually later + Network.connectToNetwork(root.network.ssid, "", root.network.bssid, null); + } else { + // No saved profile, try connecting without password first + Network.connectToNetworkWithPasswordCheck( + root.network.ssid, + root.network.isSecure, + () => { + // Callback: connection failed, show password dialog + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = root.network; + }, + root.network.bssid + ); + } + } else { + Network.connectToNetwork(root.network.ssid, "", root.network.bssid, null); + } + } } diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 1a4ba00..c64c4be 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -15,10 +15,6 @@ ColumnLayout { required property Session session - readonly property var connectionHelper: WirelessConnectionHelper { - session: root.session - } - spacing: Appearance.spacing.small RowLayout { @@ -185,7 +181,7 @@ ColumnLayout { if (modelData.active) { Network.disconnectFromNetwork(); } else { - root.connectionHelper.connectToNetwork(modelData); + handleConnect(modelData); } } } @@ -203,5 +199,45 @@ ColumnLayout { implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } + + function handleConnect(network): void { + // If already connected to a different network, disconnect first + if (Network.active && Network.active.ssid !== network.ssid) { + Network.disconnectFromNetwork(); + Qt.callLater(() => { + connectToNetwork(network); + }); + } else { + connectToNetwork(network); + } + } + + function connectToNetwork(network): void { + if (network.isSecure) { + // Check if we have a saved connection profile for this network + const hasSavedProfile = Network.savedConnections.includes(network.ssid); + + if (hasSavedProfile) { + // Try connecting with saved password - don't show dialog if it fails + // The saved password should work, but if connection fails for other reasons, + // we'll let the user try manually later + Network.connectToNetwork(network.ssid, "", network.bssid, null); + } else { + // No saved profile, try connecting without password first + Network.connectToNetworkWithPasswordCheck( + network.ssid, + network.isSecure, + () => { + // Callback: connection failed, show password dialog + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = network; + }, + network.bssid + ); + } + } else { + Network.connectToNetwork(network.ssid, "", network.bssid, null); + } + } } diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 778fb4f..df8a8cf 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -243,6 +243,32 @@ Item { } } + function checkConnectionStatus(): void { + if (!root.visible || !connectButton.connecting) { + return; + } + + // Check if we're connected to the target network + if (root.network && Network.active && Network.active.ssid === root.network.ssid) { + // Successfully connected + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + closeDialog(); + return; + } + + // Check for connection errors + const status = Network.connectionStatus; + if (status.includes("Error") || status.includes("error") || status.includes("failed")) { + // Connection failed + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + } + } + Timer { id: connectionMonitor interval: 1000 @@ -250,39 +276,15 @@ Item { triggeredOnStart: false onTriggered: { - // Check if we're connected to the target network - if (root.network && Network.active && Network.active.ssid === root.network.ssid) { - // Successfully connected - stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - closeDialog(); - } else if (connectButton.connecting) { - // Still connecting, check status - const status = Network.connectionStatus; - if (status.includes("Error") || status.includes("error") || status.includes("failed")) { - // Connection failed - stop(); - connectButton.connecting = false; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - } - } else { - // Not connecting anymore - stop(); - } + checkConnectionStatus(); } } Connections { target: Network function onActiveChanged() { - if (root.visible && root.network && Network.active && Network.active.ssid === root.network.ssid) { - // Connected successfully - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - closeDialog(); + if (root.visible) { + checkConnectionStatus(); } } } -- cgit v1.2.3-freya From 6ae1313b6b61c965ccc5f2d9d61458d7a5ed21b8 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 22:31:11 -0500 Subject: controlcenter: wireless panel refactoring --- modules/controlcenter/network/WirelessDetails.qml | 24 +- modules/controlcenter/network/WirelessList.qml | 18 +- .../network/WirelessPasswordDialog.qml | 53 +++-- services/Network.qml | 257 ++++++++++++++++++--- 4 files changed, 300 insertions(+), 52 deletions(-) diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 418c463..3e48b55 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -19,10 +19,22 @@ Item { Component.onCompleted: { updateDeviceDetails(); + checkSavedProfile(); } onNetworkChanged: { updateDeviceDetails(); + checkSavedProfile(); + } + + function checkSavedProfile(): void { + // Refresh saved connections list to ensure it's up to date + // This ensures the "Forget Network" button visibility is accurate + if (network && network.ssid) { + // Always refresh to ensure we have the latest saved connections + // This is important when networks are selected or changed + Network.listConnectionsProc.running = true; + } } Connections { @@ -80,7 +92,13 @@ Item { SimpleButton { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal - visible: root.network && root.network.ssid && Network.savedConnections.includes(root.network.ssid) + visible: { + if (!root.network || !root.network.ssid) { + return false; + } + // Check if profile exists - this will update reactively when savedConnectionSsids changes + return Network.hasSavedProfile(root.network.ssid); + } color: Colours.palette.m3errorContainer onColor: Colours.palette.m3onErrorContainer text: qsTr("Forget Network") @@ -164,8 +182,8 @@ Item { function connectToNetwork(): void { if (root.network.isSecure) { - // Check if we have a saved connection profile for this network - const hasSavedProfile = Network.savedConnections.includes(root.network.ssid); + // Check if we have a saved connection profile for this network (by SSID) + const hasSavedProfile = Network.hasSavedProfile(root.network.ssid); if (hasSavedProfile) { // Try connecting with saved password - don't show dialog if it fails diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index c64c4be..aabfc4b 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -117,6 +117,10 @@ ColumnLayout { StateLayer { function onClicked(): void { root.session.network.active = modelData; + // Check if we need to refresh saved connections when selecting a network + if (modelData && modelData.ssid) { + root.checkSavedProfileForNetwork(modelData.ssid); + } } } @@ -200,6 +204,16 @@ ColumnLayout { } } + function checkSavedProfileForNetwork(ssid: string): void { + // Refresh saved connections list to ensure it's up to date + // This ensures accurate profile detection when selecting networks + if (ssid && ssid.length > 0) { + // Always refresh to ensure we have the latest saved connections + // This is important when a network is selected from the list + Network.listConnectionsProc.running = true; + } + } + function handleConnect(network): void { // If already connected to a different network, disconnect first if (Network.active && Network.active.ssid !== network.ssid) { @@ -214,8 +228,8 @@ ColumnLayout { function connectToNetwork(network): void { if (network.isSecure) { - // Check if we have a saved connection profile for this network - const hasSavedProfile = Network.savedConnections.includes(network.ssid); + // Check if we have a saved connection profile for this network (by SSID) + const hasSavedProfile = Network.hasSavedProfile(network.ssid); if (hasSavedProfile) { // Try connecting with saved password - don't show dialog if it fails diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index df8a8cf..5bcf33c 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -248,24 +248,47 @@ Item { return; } - // Check if we're connected to the target network - if (root.network && Network.active && Network.active.ssid === root.network.ssid) { - // Successfully connected - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - closeDialog(); + // Check connection status message for success indicators + const status = Network.connectionStatus; + const statusLower = status.toLowerCase(); + + // Check for success indicators in status message + const hasSuccessIndicator = statusLower.includes("connection activated") || + statusLower.includes("successfully") || + statusLower.includes("connected successfully") || + (statusLower.includes("connected") && !statusLower.includes("error") && !statusLower.includes("failed")); + + // Check if we're connected to the target network (case-insensitive SSID comparison) + const isConnected = root.network && Network.active && Network.active.ssid && + Network.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + + if (isConnected || hasSuccessIndicator) { + // Successfully connected - give it a moment for network list to update + Qt.callLater(() => { + // Double-check connection is still active + if (root.visible && Network.active && Network.active.ssid) { + const stillConnected = Network.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected || hasSuccessIndicator) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + closeDialog(); + } + } + }, 500); return; } - // Check for connection errors - const status = Network.connectionStatus; - if (status.includes("Error") || status.includes("error") || status.includes("failed")) { - // Connection failed - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); + // Check for connection errors (but not warnings about duplicate names) + if (status.includes("Error") || (status.includes("error") && !status.includes("Warning"))) { + // Only treat as error if it's not just a warning about duplicate names + if (!status.includes("another connection with the name") && !status.includes("Reference the connection by its uuid")) { + // Connection failed + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + } } } diff --git a/services/Network.qml b/services/Network.qml index ea5c3e7..0b936b8 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -90,19 +90,26 @@ Singleton { if (hasBssid) { // Use BSSID when password is provided - ensure BSSID is uppercase const bssidUpper = bssid.toUpperCase(); - // Create connection profile with all required properties for BSSID + password - // First remove any existing connection with this name - cmd = ["nmcli", "connection", "add", - "type", "wifi", - "con-name", ssid, - "ifname", "*", - "ssid", ssid, - "802-11-wireless.bssid", bssidUpper, - "802-11-wireless-security.key-mgmt", "wpa-psk", - "802-11-wireless-security.psk", password]; - root.setConnectionStatus(qsTr("Connecting to %1 (BSSID: %2)...").arg(ssid).arg(bssidUpper)); - root.addDebugInfo(qsTr("Using BSSID: %1 for SSID: %2").arg(bssidUpper).arg(ssid)); - root.addDebugInfo(qsTr("Creating connection profile with password and key-mgmt")); + + // Check if a connection with this SSID already exists + const existingConnection = root.savedConnections.find(conn => + conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() + ); + + if (existingConnection) { + // Connection already exists - delete it first, then create new one with updated password + root.addDebugInfo(qsTr("Connection '%1' already exists, deleting it first...").arg(existingConnection)); + deleteConnectionProc.exec(["nmcli", "connection", "delete", existingConnection]); + // Wait a moment for deletion to complete, then create new connection + Qt.callLater(() => { + createConnectionWithPassword(ssid, bssidUpper, password); + }, 300); + return; + } else { + // No existing connection, create new one + createConnectionWithPassword(ssid, bssidUpper, password); + return; + } } else { // Fallback to SSID if BSSID not available - use device wifi connect cmd = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; @@ -164,6 +171,29 @@ Singleton { root.addDebugInfo(qsTr("No callback provided - not starting connection check timer")); } } + + function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string): void { + // Create connection profile with all required properties for BSSID + password + const cmd = ["nmcli", "connection", "add", + "type", "wifi", + "con-name", ssid, + "ifname", "*", + "ssid", ssid, + "802-11-wireless.bssid", bssidUpper, + "802-11-wireless-security.key-mgmt", "wpa-psk", + "802-11-wireless-security.psk", password]; + + root.setConnectionStatus(qsTr("Connecting to %1 (BSSID: %2)...").arg(ssid).arg(bssidUpper)); + root.addDebugInfo(qsTr("Using BSSID: %1 for SSID: %2").arg(bssidUpper).arg(ssid)); + root.addDebugInfo(qsTr("Creating connection profile with password and key-mgmt")); + + // Set command and start process + connectProc.command = cmd; + + Qt.callLater(() => { + connectProc.running = true; + }); + } function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { root.addDebugInfo(qsTr("=== connectToNetworkWithPasswordCheck ===")); @@ -224,23 +254,150 @@ Singleton { } property list savedConnections: [] + property list savedConnectionSsids: [] + property var wifiConnectionQueue: [] + property int currentSsidQueryIndex: 0 Process { id: listConnectionsProc - command: ["nmcli", "-t", "-f", "NAME", "connection", "show"] + command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + onExited: { + if (exitCode === 0) { + parseConnectionList(stdout.text); + } + } + stdout: StdioCollector { + onStreamFinished: { + parseConnectionList(text); + } + } + } + + function parseConnectionList(output: string): void { + const lines = output.trim().split("\n").filter(line => line.length > 0); + const wifiConnections = []; + const connections = []; + + // First pass: identify WiFi connections + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2) { + const name = parts[0]; + const type = parts[1]; + connections.push(name); + + if (type === "802-11-wireless") { + wifiConnections.push(name); + } + } + } + + root.savedConnections = connections; + + // Second pass: get SSIDs for WiFi connections + if (wifiConnections.length > 0) { + root.wifiConnectionQueue = wifiConnections; + root.currentSsidQueryIndex = 0; + root.savedConnectionSsids = []; + // Start querying SSIDs one by one + queryNextSsid(); + } else { + root.savedConnectionSsids = []; + root.wifiConnectionQueue = []; + } + } + + Process { + id: getSsidProc + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) onExited: { if (exitCode === 0) { - // Parse connection names from output - const connections = stdout.text.trim().split("\n").filter(name => name.length > 0); - root.savedConnections = connections; + processSsidOutput(stdout.text); + } else { + // Move to next connection even if this one failed + queryNextSsid(); } } stdout: StdioCollector { onStreamFinished: { - const connections = text.trim().split("\n").filter(name => name.length > 0); - root.savedConnections = connections; + processSsidOutput(text); + } + } + } + + function processSsidOutput(output: string): void { + // Parse "802-11-wireless.ssid:SSID_NAME" format + const lines = output.trim().split("\n"); + for (const line of lines) { + if (line.startsWith("802-11-wireless.ssid:")) { + const ssid = line.substring("802-11-wireless.ssid:".length).trim(); + if (ssid && ssid.length > 0) { + // Add to list if not already present (case-insensitive) + const ssidLower = ssid.toLowerCase(); + if (!root.savedConnectionSsids.some(s => s && s.toLowerCase() === ssidLower)) { + // Create new array to trigger QML property change notification + const newList = root.savedConnectionSsids.slice(); + newList.push(ssid); + root.savedConnectionSsids = newList; + } + } + } + } + + // Query next connection + queryNextSsid(); + } + + function queryNextSsid(): void { + if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { + const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; + root.currentSsidQueryIndex++; + getSsidProc.command = ["nmcli", "-t", "-f", "802-11-wireless.ssid", "connection", "show", connectionName]; + getSsidProc.running = true; + } else { + // All SSIDs retrieved + root.wifiConnectionQueue = []; + root.currentSsidQueryIndex = 0; + } + } + + function hasSavedProfile(ssid: string): bool { + if (!ssid || ssid.length === 0) { + return false; + } + const ssidLower = ssid.toLowerCase().trim(); + + // If currently connected to this network, it definitely has a saved profile + if (root.active && root.active.ssid) { + const activeSsidLower = root.active.ssid.toLowerCase().trim(); + if (activeSsidLower === ssidLower) { + return true; } } + + // Check if SSID is in saved connections (case-insensitive comparison) + const hasSsid = root.savedConnectionSsids.some(savedSsid => + savedSsid && savedSsid.toLowerCase().trim() === ssidLower + ); + + if (hasSsid) { + return true; + } + + // Fallback: also check if connection name matches SSID (some connections use SSID as name) + const hasConnectionName = root.savedConnections.some(connName => + connName && connName.toLowerCase().trim() === ssidLower + ); + + return hasConnectionName; } function getWifiStatus(): void { @@ -445,22 +602,58 @@ Singleton { && connectProc.command[1] === "connection" && connectProc.command[2] === "add"; - if (wasConnectionAdd && exitCode === 0 && root.pendingConnection) { - // Connection profile was created successfully, now activate it + if (wasConnectionAdd && root.pendingConnection) { const ssid = root.pendingConnection.ssid; - root.addDebugInfo(qsTr("Connection profile created successfully, now activating: %1").arg(ssid)); - root.setConnectionStatus(qsTr("Activating connection...")); - // Update saved connections list since we just created one - listConnectionsProc.running = true; + // Check for duplicate connection warning in stderr text + const stderrText = connectProc.stderr ? connectProc.stderr.text : ""; + const hasDuplicateWarning = stderrText && ( + stderrText.includes("another connection with the name") || + stderrText.includes("Reference the connection by its uuid") + ); - // Activate the connection we just created - connectProc.command = ["nmcli", "connection", "up", ssid]; - Qt.callLater(() => { - connectProc.running = true; - }); - // Don't start timers yet - wait for activation to complete - return; + // Even with duplicate warning (or if connection already exists), we should try to activate it + // Also try if exit code is non-zero but small (might be a warning, not a real error) + if (exitCode === 0 || hasDuplicateWarning || (exitCode > 0 && exitCode < 10)) { + if (hasDuplicateWarning) { + root.addDebugInfo(qsTr("Connection with name '%1' already exists (warning), will try to activate it").arg(ssid)); + root.setConnectionStatus(qsTr("Activating existing connection...")); + } else { + root.addDebugInfo(qsTr("Connection profile created successfully, now activating: %1").arg(ssid)); + root.setConnectionStatus(qsTr("Activating connection...")); + } + + // Update saved connections list + listConnectionsProc.running = true; + + // Try to activate the connection by SSID (connection name) + connectProc.command = ["nmcli", "connection", "up", ssid]; + Qt.callLater(() => { + connectProc.running = true; + }); + // Don't start timers yet - wait for activation to complete + return; + } else { + // Connection add failed - try using device wifi connect as fallback + root.addDebugInfo(qsTr("Connection add failed (exit code %1), trying device wifi connect as fallback").arg(exitCode)); + // Extract password from the command if available + let password = ""; + if (connectProc.command) { + const pskIndex = connectProc.command.findIndex(arg => arg === "802-11-wireless-security.psk"); + if (pskIndex >= 0 && pskIndex + 1 < connectProc.command.length) { + password = connectProc.command[pskIndex + 1]; + } + } + + if (password && password.length > 0) { + root.addDebugInfo(qsTr("Using device wifi connect with password as fallback")); + connectProc.command = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; + Qt.callLater(() => { + connectProc.running = true; + }); + return; + } + } } // Refresh network list after connection attempt -- cgit v1.2.3-freya From 0a33ecf4337fc52ad83c0d5d977fdf4df06b2616 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 12 Nov 2025 22:41:10 -0500 Subject: tray: minor button updates to match --- modules/bar/popouts/Audio.qml | 39 ++++++------------------- modules/bar/popouts/Bluetooth.qml | 40 ++++++-------------------- modules/controlcenter/network/SimpleButton.qml | 27 +++++++++++++---- 3 files changed, 39 insertions(+), 67 deletions(-) diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 0954b10..827f868 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -9,6 +9,7 @@ import Quickshell.Services.Pipewire import QtQuick import QtQuick.Layouts import QtQuick.Controls +import "../../controlcenter/network" Item { id: root @@ -104,40 +105,16 @@ Item { } } - StyledRect { + SimpleButton { + Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal - - implicitWidth: expandBtn.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: expandBtn.implicitHeight + Appearance.padding.small - - radius: Appearance.rounding.normal color: Colours.palette.m3primaryContainer + onColor: Colours.palette.m3onPrimaryContainer + text: qsTr("Open Settings") + icon: "chevron_right" - StateLayer { - color: Colours.palette.m3onPrimaryContainer - - function onClicked(): void { - root.wrapper.detach("audio"); - } - } - - RowLayout { - id: expandBtn - - anchors.centerIn: parent - spacing: Appearance.spacing.small - - StyledText { - Layout.leftMargin: Appearance.padding.smaller - text: qsTr("Open Settings") - color: Colours.palette.m3onPrimaryContainer - } - - MaterialIcon { - text: "chevron_right" - color: Colours.palette.m3onPrimaryContainer - font.pointSize: Appearance.font.size.large - } + onClicked: { + root.wrapper.detach("audio"); } } } diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index a43bba1..60af466 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -9,6 +9,7 @@ import Quickshell import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts +import "../../controlcenter/network" ColumnLayout { id: root @@ -164,39 +165,16 @@ ColumnLayout { } } - StyledRect { - Layout.topMargin: Appearance.spacing.small - implicitWidth: expandBtn.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: expandBtn.implicitHeight + Appearance.padding.small - - radius: Appearance.rounding.normal + SimpleButton { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal color: Colours.palette.m3primaryContainer + onColor: Colours.palette.m3onPrimaryContainer + text: qsTr("Open Settings") + icon: "chevron_right" - StateLayer { - color: Colours.palette.m3onPrimaryContainer - - function onClicked(): void { - root.wrapper.detach("bluetooth"); - } - } - - RowLayout { - id: expandBtn - - anchors.centerIn: parent - spacing: Appearance.spacing.small - - StyledText { - Layout.leftMargin: Appearance.padding.smaller - text: qsTr("Open Settings") - color: Colours.palette.m3onPrimaryContainer - } - - MaterialIcon { - text: "chevron_right" - color: Colours.palette.m3onPrimaryContainer - font.pointSize: Appearance.font.size.large - } + onClicked: { + root.wrapper.detach("bluetooth"); } } diff --git a/modules/controlcenter/network/SimpleButton.qml b/modules/controlcenter/network/SimpleButton.qml index 49ab2f4..c949db3 100644 --- a/modules/controlcenter/network/SimpleButton.qml +++ b/modules/controlcenter/network/SimpleButton.qml @@ -1,7 +1,9 @@ import qs.components +import qs.components.controls import qs.components.effects import qs.config import QtQuick +import QtQuick.Layouts StyledRect { id: root @@ -10,9 +12,10 @@ StyledRect { property alias disabled: stateLayer.disabled property alias text: label.text property alias enabled: stateLayer.enabled + property string icon: "" - implicitWidth: label.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: label.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: rowLayout.implicitHeight + Appearance.padding.small radius: Appearance.rounding.normal StateLayer { @@ -25,10 +28,24 @@ StyledRect { } } - StyledText { - id: label + RowLayout { + id: rowLayout anchors.centerIn: parent - color: parent.onColor + spacing: Appearance.spacing.small + + StyledText { + id: label + Layout.leftMargin: root.icon.length > 0 ? Appearance.padding.smaller : 0 + color: parent.parent.onColor + } + + MaterialIcon { + id: iconItem + visible: root.icon.length > 0 + text: root.icon + color: root.onColor + font.pointSize: Appearance.font.size.large + } } signal clicked -- cgit v1.2.3-freya From 81203ba7bccdc367b03a40f492b99bfad0445bda Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 08:11:27 -0500 Subject: tray: changed settings icon in button --- modules/bar/popouts/Audio.qml | 2 +- modules/bar/popouts/Bluetooth.qml | 2 +- modules/controlcenter/network/SimpleButton.qml | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 827f868..20e98e5 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -111,7 +111,7 @@ Item { color: Colours.palette.m3primaryContainer onColor: Colours.palette.m3onPrimaryContainer text: qsTr("Open Settings") - icon: "chevron_right" + icon: "settings" onClicked: { root.wrapper.detach("audio"); diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 60af466..c1f2029 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -171,7 +171,7 @@ ColumnLayout { color: Colours.palette.m3primaryContainer onColor: Colours.palette.m3onPrimaryContainer text: qsTr("Open Settings") - icon: "chevron_right" + icon: "settings" onClicked: { root.wrapper.detach("bluetooth"); diff --git a/modules/controlcenter/network/SimpleButton.qml b/modules/controlcenter/network/SimpleButton.qml index c949db3..00e5a4e 100644 --- a/modules/controlcenter/network/SimpleButton.qml +++ b/modules/controlcenter/network/SimpleButton.qml @@ -33,12 +33,6 @@ StyledRect { anchors.centerIn: parent spacing: Appearance.spacing.small - StyledText { - id: label - Layout.leftMargin: root.icon.length > 0 ? Appearance.padding.smaller : 0 - color: parent.parent.onColor - } - MaterialIcon { id: iconItem visible: root.icon.length > 0 @@ -46,6 +40,12 @@ StyledRect { color: root.onColor font.pointSize: Appearance.font.size.large } + + StyledText { + id: label + Layout.leftMargin: root.icon.length > 0 ? Appearance.padding.smaller : 0 + color: parent.parent.onColor + } } signal clicked -- cgit v1.2.3-freya From b88b6027d2f42e8b382e18a9ff80877be838a37e Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 10:31:38 -0500 Subject: fix: background not loading on config reload --- modules/background/Background.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/background/Background.qml b/modules/background/Background.qml index fbacfab..3e6d933 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -9,7 +9,6 @@ import Quickshell.Wayland import QtQuick Loader { - asynchronous: true active: Config.background.enabled sourceComponent: Variants { -- cgit v1.2.3-freya From 2ebdba6cc4a6b3fe6acd3498c774d2ed8678eb2a Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 13:57:35 -0500 Subject: launcher: reverted python launcher item to not exist, changes back to c0ea060f --- config/LauncherConfig.qml | 8 --- modules/launcher/AppList.qml | 18 +---- modules/launcher/Content.qml | 2 +- modules/launcher/items/PythonItem.qml | 124 ---------------------------------- 4 files changed, 2 insertions(+), 150 deletions(-) delete mode 100644 modules/launcher/items/PythonItem.qml diff --git a/config/LauncherConfig.qml b/config/LauncherConfig.qml index 30b3e8b..9d9c50c 100644 --- a/config/LauncherConfig.qml +++ b/config/LauncherConfig.qml @@ -38,14 +38,6 @@ JsonObject { enabled: true, dangerous: false }, - { - name: "Python", - icon: "code", - description: "Execute Python one-liners", - command: ["autocomplete", "python"], - enabled: true, - dangerous: false - }, { name: "Scheme", icon: "palette", diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index 6217237..7f7b843 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -52,7 +52,7 @@ StyledListView { const text = search.text; const prefix = Config.launcher.actionPrefix; if (text.startsWith(prefix)) { - for (const action of ["calc", "python", "scheme", "variant"]) + for (const action of ["calc", "scheme", "variant"]) if (text.startsWith(`${prefix}${action} `)) return action; @@ -92,14 +92,6 @@ StyledListView { root.delegate: calcItem } }, - State { - name: "python" - - PropertyChanges { - model.values: [0] - root.delegate: pythonItem - } - }, State { name: "scheme" @@ -247,14 +239,6 @@ StyledListView { } } - Component { - id: pythonItem - - PythonItem { - list: root - } - } - Component { id: schemeItem diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 3a90a74..f674569 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -90,7 +90,7 @@ Item { Wallpapers.setWallpaper(currentItem.modelData.path); root.visibilities.launcher = false; } else if (text.startsWith(Config.launcher.actionPrefix)) { - if (text.startsWith(`${Config.launcher.actionPrefix}calc `) || text.startsWith(`${Config.launcher.actionPrefix}python `)) + if (text.startsWith(`${Config.launcher.actionPrefix}calc `)) currentItem.onClicked(); else currentItem.modelData.onClicked(list.currentList); diff --git a/modules/launcher/items/PythonItem.qml b/modules/launcher/items/PythonItem.qml deleted file mode 100644 index 8384e71..0000000 --- a/modules/launcher/items/PythonItem.qml +++ /dev/null @@ -1,124 +0,0 @@ -import qs.components -import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts - -Item { - id: root - - required property var list - readonly property string code: list.search.text.slice(`${Config.launcher.actionPrefix}python `.length) - - function onClicked(): void { - // Execute Python code and copy result to clipboard - // Escape single quotes in code for shell safety - const escapedCode = root.code.replace(/'/g, "'\\''"); - Quickshell.execDetached(["sh", "-c", `python3 -c '${escapedCode}' 2>&1 | wl-copy`]); - root.list.visibilities.launcher = false; - } - - implicitHeight: Config.launcher.sizes.itemHeight - - anchors.left: parent?.left - anchors.right: parent?.right - - StateLayer { - radius: Appearance.rounding.normal - - function onClicked(): void { - root.onClicked(); - } - } - - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.larger - - spacing: Appearance.spacing.normal - - MaterialIcon { - text: "code" - font.pointSize: Appearance.font.size.extraLarge - Layout.alignment: Qt.AlignVCenter - } - - StyledText { - id: result - - color: { - if (!root.code) - return Colours.palette.m3onSurfaceVariant; - return Colours.palette.m3onSurface; - } - - text: root.code.length > 0 ? qsTr("Press Enter to execute: %1").arg(root.code) : qsTr("Type Python code to execute") - elide: Text.ElideLeft - - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - } - - StyledRect { - color: Colours.palette.m3tertiary - radius: Appearance.rounding.normal - clip: true - - implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Appearance.padding.small * 2 - - Layout.alignment: Qt.AlignVCenter - - StateLayer { - id: stateLayer - - color: Colours.palette.m3onTertiary - - function onClicked(): void { - const escapedCode = root.code.replace(/'/g, "'\\''"); - Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `python3 -i -c '${escapedCode}'`]); - root.list.visibilities.launcher = false; - } - } - - StyledText { - id: label - - anchors.verticalCenter: parent.verticalCenter - anchors.right: icon.left - anchors.rightMargin: Appearance.spacing.small - - text: qsTr("Open in terminal") - color: Colours.palette.m3onTertiary - font.pointSize: Appearance.font.size.normal - - opacity: stateLayer.containsMouse ? 1 : 0 - - Behavior on opacity { - Anim {} - } - } - - MaterialIcon { - id: icon - - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: Appearance.padding.normal - - text: "open_in_new" - color: Colours.palette.m3onTertiary - font.pointSize: Appearance.font.size.large - } - - Behavior on implicitWidth { - Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - } - } -} -- cgit v1.2.3-freya From e9113a1710af3dd031c3fa413059d70aed18c4e5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 14:12:34 -0500 Subject: cleanup: trailing whitespace --- modules/controlcenter/audio/AudioPane.qml | 4 +--- modules/controlcenter/ethernet/EthernetDetails.qml | 26 +--------------------- modules/controlcenter/ethernet/EthernetList.qml | 23 +------------------ modules/controlcenter/ethernet/EthernetPane.qml | 10 +-------- .../controlcenter/ethernet/EthernetSettings.qml | 23 +------------------ modules/controlcenter/network/SimpleButton.qml | 3 +-- modules/controlcenter/network/WirelessDetails.qml | 3 +-- modules/controlcenter/network/WirelessList.qml | 3 +-- modules/controlcenter/network/WirelessPane.qml | 3 +-- .../network/WirelessPasswordDialog.qml | 3 +-- modules/controlcenter/network/WirelessSettings.qml | 3 +-- 11 files changed, 11 insertions(+), 93 deletions(-) diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 6c478e5..502134a 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -345,6 +345,4 @@ RowLayout { leftThickness: Appearance.padding.normal / 2 } } -} - - +} \ No newline at end of file diff --git a/modules/controlcenter/ethernet/EthernetDetails.qml b/modules/controlcenter/ethernet/EthernetDetails.qml index a49eb4f..d9a004b 100644 --- a/modules/controlcenter/ethernet/EthernetDetails.qml +++ b/modules/controlcenter/ethernet/EthernetDetails.qml @@ -110,28 +110,4 @@ Item { } } -} - - - - - - - - - - - - - - - - - - - - - - - - +} \ No newline at end of file diff --git a/modules/controlcenter/ethernet/EthernetList.qml b/modules/controlcenter/ethernet/EthernetList.qml index 8b04c09..b8c485b 100644 --- a/modules/controlcenter/ethernet/EthernetList.qml +++ b/modules/controlcenter/ethernet/EthernetList.qml @@ -166,25 +166,4 @@ ColumnLayout { implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } -} - - - - - - - - - - - - - - - - - - - - - +} \ No newline at end of file diff --git a/modules/controlcenter/ethernet/EthernetPane.qml b/modules/controlcenter/ethernet/EthernetPane.qml index fc3e1c0..b3ff317 100644 --- a/modules/controlcenter/ethernet/EthernetPane.qml +++ b/modules/controlcenter/ethernet/EthernetPane.qml @@ -145,12 +145,4 @@ RowLayout { duration: Appearance.anim.durations.normal / 2 easing.type: Easing.BezierSpline } -} - - - - - - - - +} \ No newline at end of file diff --git a/modules/controlcenter/ethernet/EthernetSettings.qml b/modules/controlcenter/ethernet/EthernetSettings.qml index 33b1449..68a1f61 100644 --- a/modules/controlcenter/ethernet/EthernetSettings.qml +++ b/modules/controlcenter/ethernet/EthernetSettings.qml @@ -81,25 +81,4 @@ ColumnLayout { } } } -} - - - - - - - - - - - - - - - - - - - - - +} \ No newline at end of file diff --git a/modules/controlcenter/network/SimpleButton.qml b/modules/controlcenter/network/SimpleButton.qml index 00e5a4e..7d85e4f 100644 --- a/modules/controlcenter/network/SimpleButton.qml +++ b/modules/controlcenter/network/SimpleButton.qml @@ -49,5 +49,4 @@ StyledRect { } signal clicked -} - +} \ No newline at end of file diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 3e48b55..7039720 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -207,5 +207,4 @@ Item { Network.connectToNetwork(root.network.ssid, "", root.network.bssid, null); } } -} - +} \ No newline at end of file diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index aabfc4b..f861db4 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -253,5 +253,4 @@ ColumnLayout { Network.connectToNetwork(network.ssid, "", network.bssid, null); } } -} - +} \ No newline at end of file diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index a23b6e8..9d48729 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -151,5 +151,4 @@ RowLayout { duration: Appearance.anim.durations.normal / 2 easing.type: Easing.BezierSpline } -} - +} \ No newline at end of file diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 5bcf33c..2b33b43 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -320,5 +320,4 @@ Item { connectionMonitor.stop(); Network.clearConnectionStatus(); } -} - +} \ No newline at end of file diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml index 7890099..073b09c 100644 --- a/modules/controlcenter/network/WirelessSettings.qml +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -78,5 +78,4 @@ ColumnLayout { value: Network.active ? qsTr("%1 MHz").arg(Network.active.frequency) : qsTr("N/A") } } -} - +} \ No newline at end of file -- cgit v1.2.3-freya From 1da9c68be8f336a671f9514cf5feaaf5998da981 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 14:41:14 -0500 Subject: cleanup: trailing whitespace removeal (entire project) --- components/controls/CollapsibleSection.qml | 2 +- .../controlcenter/appearance/AppearancePane.qml | 50 +++---- modules/controlcenter/launcher/LauncherPane.qml | 10 +- modules/controlcenter/network/WirelessDetails.qml | 6 +- modules/controlcenter/network/WirelessList.qml | 2 +- .../network/WirelessPasswordDialog.qml | 10 +- modules/controlcenter/taskbar/TaskbarPane.qml | 8 +- modules/drawers/Interactions.qml | 22 +-- services/Network.qml | 154 ++++++++++----------- services/VPN.qml | 6 +- utils/Icons.qml | 4 +- 11 files changed, 137 insertions(+), 137 deletions(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 945386c..cb6e62a 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -12,7 +12,7 @@ ColumnLayout { required property string title property string description: "" property bool expanded: false - + signal toggleRequested spacing: Appearance.spacing.small / 2 diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 68e2e2d..fc338f9 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -310,20 +310,20 @@ RowLayout { StateLayer { function onClicked(): void { const variant = modelData.variant; - + // Optimistic update - set immediately Schemes.currentVariant = variant; - + // Execute the command Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); - + // Reload after a delay to confirm Qt.callLater(() => { reloadTimer.restart(); }); } } - + Timer { id: reloadTimer interval: 300 @@ -410,20 +410,20 @@ RowLayout { const name = modelData.name; const flavour = modelData.flavour; const schemeKey = `${name} ${flavour}`; - + // Optimistic update - set immediately Schemes.currentScheme = schemeKey; - + // Execute the command Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - + // Reload after a delay to confirm Qt.callLater(() => { reloadTimer.restart(); }); } } - + Timer { id: reloadTimer interval: 300 @@ -1053,7 +1053,7 @@ RowLayout { columns: Math.max(1, Math.floor(parent.width / 200)) rowSpacing: Appearance.spacing.normal columnSpacing: Appearance.spacing.normal - + // Center the grid content Layout.maximumWidth: { const cols = columns; @@ -1100,16 +1100,16 @@ RowLayout { path: modelData.path anchors.fill: parent - + // Ensure sourceSize is always set to valid dimensions sourceSize: Qt.size( Math.max(1, Math.floor(parent.width)), Math.max(1, Math.floor(parent.height)) ) - + // Show when ready, hide if fallback is showing opacity: status === Image.Ready && !fallbackImage.visible ? 1 : 0 - + Behavior on opacity { NumberAnimation { duration: 200 @@ -1129,11 +1129,11 @@ RowLayout { Math.max(1, Math.floor(parent.width)), Math.max(1, Math.floor(parent.height)) ) - + // Show if caching image hasn't loaded after a delay visible: opacity > 0 opacity: 0 - + Timer { id: fallbackTimer interval: 500 @@ -1144,7 +1144,7 @@ RowLayout { } } } - + // Also check status changes onStatusChanged: { if (status === Image.Ready && cachingImage.status !== Image.Ready) { @@ -1155,7 +1155,7 @@ RowLayout { }); } } - + Behavior on opacity { NumberAnimation { duration: 200 @@ -1182,26 +1182,26 @@ RowLayout { anchors.right: parent.right anchors.bottom: parent.bottom height: filenameText.implicitHeight + Appearance.padding.normal * 2 - + // Match the parent's rounded corners at the bottom radius: Appearance.rounding.normal - + gradient: Gradient { GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0) } GradientStop { position: 0.3; color: Qt.rgba(0, 0, 0, 0.3) } GradientStop { position: 0.7; color: Qt.rgba(0, 0, 0, 0.75) } GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.85) } } - + opacity: 0 - + Behavior on opacity { NumberAnimation { duration: 200 easing.type: Easing.OutCubic } } - + Component.onCompleted: { opacity = 1; } @@ -1228,20 +1228,20 @@ RowLayout { color: isCurrent ? Colours.palette.m3primary : "#FFFFFF" elide: Text.ElideMiddle maximumLineCount: 1 - + // Text shadow for better readability style: Text.Outline styleColor: Qt.rgba(0, 0, 0, 0.6) - + opacity: 0 - + Behavior on opacity { NumberAnimation { duration: 200 easing.type: Easing.OutCubic } } - + Component.onCompleted: { opacity = 1; } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 9b2570a..dd00877 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -53,7 +53,7 @@ RowLayout { try { const config = JSON.parse(configFile.text()); const appId = root.selectedApp.id || root.selectedApp.entry?.id; - + if (config.launcher && config.launcher.hiddenApps) { root.hideFromLauncherChecked = config.launcher.hiddenApps.includes(appId); } else { @@ -72,12 +72,12 @@ RowLayout { try { const config = JSON.parse(configFile.text()); const appId = root.selectedApp.id || root.selectedApp.entry?.id; - + if (!config.launcher) config.launcher = {}; if (!config.launcher.hiddenApps) config.launcher.hiddenApps = []; - + const hiddenApps = config.launcher.hiddenApps; - + if (isHidden) { // Add to hiddenApps if not already there if (!hiddenApps.includes(appId)) { @@ -90,7 +90,7 @@ RowLayout { hiddenApps.splice(index, 1); } } - + const jsonString = JSON.stringify(config, null, 4); configFile.setText(jsonString); } catch (e) { diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 7039720..d5abc9d 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -26,7 +26,7 @@ Item { updateDeviceDetails(); checkSavedProfile(); } - + function checkSavedProfile(): void { // Refresh saved connections list to ensure it's up to date // This ensures the "Forget Network" button visibility is accurate @@ -102,7 +102,7 @@ Item { color: Colours.palette.m3errorContainer onColor: Colours.palette.m3onErrorContainer text: qsTr("Forget Network") - + onClicked: { if (root.network && root.network.ssid) { // Disconnect first if connected @@ -184,7 +184,7 @@ Item { if (root.network.isSecure) { // Check if we have a saved connection profile for this network (by SSID) const hasSavedProfile = Network.hasSavedProfile(root.network.ssid); - + if (hasSavedProfile) { // Try connecting with saved password - don't show dialog if it fails // The saved password should work, but if connection fails for other reasons, diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index f861db4..ca6947a 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -230,7 +230,7 @@ ColumnLayout { if (network.isSecure) { // Check if we have a saved connection profile for this network (by SSID) const hasSavedProfile = Network.hasSavedProfile(network.ssid); - + if (hasSavedProfile) { // Try connecting with saved password - don't show dialog if it fails // The saved password should work, but if connection fails for other reasons, diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 2b33b43..8a71fa8 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -15,7 +15,7 @@ Item { id: root required property Session session - + readonly property var network: { // Prefer pendingNetwork, then active network if (session.network.pendingNetwork) { @@ -105,7 +105,7 @@ Item { StyledText { id: statusText - + Layout.alignment: Qt.AlignHCenter Layout.topMargin: Appearance.spacing.small visible: Network.connectionStatus.length > 0 || connectButton.connecting @@ -251,15 +251,15 @@ Item { // Check connection status message for success indicators const status = Network.connectionStatus; const statusLower = status.toLowerCase(); - + // Check for success indicators in status message - const hasSuccessIndicator = statusLower.includes("connection activated") || + const hasSuccessIndicator = statusLower.includes("connection activated") || statusLower.includes("successfully") || statusLower.includes("connected successfully") || (statusLower.includes("connected") && !statusLower.includes("error") && !statusLower.includes("failed")); // Check if we're connected to the target network (case-insensitive SSID comparison) - const isConnected = root.network && Network.active && Network.active.ssid && + const isConnected = root.network && Network.active && Network.active.ssid && Network.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); if (isConnected || hasSuccessIndicator) { diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 2bb50d8..cf52fd3 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -120,13 +120,13 @@ RowLayout { if (!configFile.loaded) { return; } - + try { const config = JSON.parse(configFile.text()); - + // Ensure bar object exists if (!config.bar) config.bar = {}; - + // Update clock setting if (!config.bar.clock) config.bar.clock = {}; config.bar.clock.showIcon = clockShowIconSwitch.checked; @@ -163,7 +163,7 @@ RowLayout { // Update entries from the model (same approach as clock - use provided value if available) if (!config.bar.entries) config.bar.entries = []; config.bar.entries = []; - + for (let i = 0; i < entriesModel.count; i++) { const entry = entriesModel.get(i); // If this is the entry being updated, use the provided value (same as clock toggle reads from switch) diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 10190a4..2d0c115 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -204,18 +204,18 @@ CustomMouseArea { const panelWidth = panels.notifications.width || panels.notifications.implicitWidth || Config.notifs.sizes.width; const panelX = bar.implicitWidth + panels.notifications.x; const isPanelCollapsed = panelHeight < 10; // Consider collapsed if height is very small - + let showNotifications = inTopPanel(panels.notifications, x, y); - + // Only use fallback corner detection when panel is collapsed if (!showNotifications && isPanelCollapsed) { // Use panel's actual width and position for fallback, with some padding const cornerPadding = Config.border.rounding || 20; - showNotifications = x >= panelX - cornerPadding && - x <= panelX + panelWidth + cornerPadding && + showNotifications = x >= panelX - cornerPadding && + x <= panelX + panelWidth + cornerPadding && y < Config.border.thickness + cornerPadding; } - + // Check if mouse is over the clear all button area // Button is positioned to the left of the notification panel if (!showNotifications && panels.notifications.height > 0 && panels.clearAllButton && panels.clearAllButton.visible) { @@ -223,17 +223,17 @@ CustomMouseArea { const buttonY = Config.border.thickness + panels.clearAllButton.y; const buttonWidth = panels.clearAllButton.width; const buttonHeight = panels.clearAllButton.height; - - const inButtonArea = x >= buttonX && - x <= buttonX + buttonWidth && - y >= buttonY && + + const inButtonArea = x >= buttonX && + x <= buttonX + buttonWidth && + y >= buttonY && y <= buttonY + buttonHeight; - + if (inButtonArea) { showNotifications = true; } } - + // Show or hide notification panel based on hover if (panels.notifications.content) { if (showNotifications) { diff --git a/services/Network.qml b/services/Network.qml index 0b936b8..7732a1c 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -30,17 +30,17 @@ Singleton { property var wirelessDeviceDetails: null property string connectionStatus: "" property string connectionDebug: "" - + function clearConnectionStatus(): void { connectionStatus = ""; // Don't clear debug - keep it for reference // connectionDebug = ""; } - + function setConnectionStatus(status: string): void { connectionStatus = status; } - + function addDebugInfo(info: string): void { const timestamp = new Date().toLocaleTimeString(); const newInfo = "[" + timestamp + "] " + info; @@ -79,23 +79,23 @@ Singleton { // When no password, use SSID (will use saved password if available) const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; let cmd = []; - + // Set up pending connection tracking if callback provided if (callback) { root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; } - + if (password && password.length > 0) { // When password is provided, try BSSID first if available, otherwise use SSID if (hasBssid) { // Use BSSID when password is provided - ensure BSSID is uppercase const bssidUpper = bssid.toUpperCase(); - + // Check if a connection with this SSID already exists - const existingConnection = root.savedConnections.find(conn => + const existingConnection = root.savedConnections.find(conn => conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() ); - + if (existingConnection) { // Connection already exists - delete it first, then create new one with updated password root.addDebugInfo(qsTr("Connection '%1' already exists, deleting it first...").arg(existingConnection)); @@ -122,7 +122,7 @@ Singleton { root.setConnectionStatus(qsTr("Connecting to %1 (using saved password)...").arg(ssid)); root.addDebugInfo(qsTr("Using saved password for: %1").arg(ssid)); } - + // Show the exact command being executed const cmdStr = cmd.join(" "); root.addDebugInfo(qsTr("=== COMMAND TO EXECUTE ===")); @@ -130,17 +130,17 @@ Singleton { root.addDebugInfo(qsTr("Command array: [%1]").arg(cmd.map((arg, i) => `"${arg}"`).join(", "))); root.addDebugInfo(qsTr("Command array length: %1").arg(cmd.length)); root.addDebugInfo(qsTr("===========================")); - + // Set command and start process root.addDebugInfo(qsTr("Setting command property...")); connectProc.command = cmd; const setCmdStr = connectProc.command ? connectProc.command.join(" ") : "null"; root.addDebugInfo(qsTr("Command property set, value: %1").arg(setCmdStr)); root.addDebugInfo(qsTr("Command property verified: %1").arg(setCmdStr === cmdStr ? "Match" : "MISMATCH")); - + // If we're creating a connection profile, we need to activate it after creation const isConnectionAdd = cmd.length > 0 && cmd[0] === "nmcli" && cmd[1] === "connection" && cmd[2] === "add"; - + // Wait a moment before starting to ensure command is set Qt.callLater(() => { root.addDebugInfo(qsTr("=== STARTING PROCESS ===")); @@ -150,7 +150,7 @@ Singleton { connectProc.running = true; root.addDebugInfo(qsTr("Process running set to: %1").arg(connectProc.running)); root.addDebugInfo(qsTr("========================")); - + // Check if process actually started after a short delay Qt.callLater(() => { root.addDebugInfo(qsTr("Process status check (100ms later):")); @@ -162,7 +162,7 @@ Singleton { } }, 100); }); - + // Start connection check timer if we have a callback if (callback) { root.addDebugInfo(qsTr("Starting connection check timer (4 second interval)")); @@ -171,25 +171,25 @@ Singleton { root.addDebugInfo(qsTr("No callback provided - not starting connection check timer")); } } - + function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string): void { // Create connection profile with all required properties for BSSID + password - const cmd = ["nmcli", "connection", "add", - "type", "wifi", + const cmd = ["nmcli", "connection", "add", + "type", "wifi", "con-name", ssid, "ifname", "*", "ssid", ssid, "802-11-wireless.bssid", bssidUpper, "802-11-wireless-security.key-mgmt", "wpa-psk", "802-11-wireless-security.psk", password]; - + root.setConnectionStatus(qsTr("Connecting to %1 (BSSID: %2)...").arg(ssid).arg(bssidUpper)); root.addDebugInfo(qsTr("Using BSSID: %1 for SSID: %2").arg(bssidUpper).arg(ssid)); root.addDebugInfo(qsTr("Creating connection profile with password and key-mgmt")); - + // Set command and start process connectProc.command = cmd; - + Qt.callLater(() => { connectProc.running = true; }); @@ -198,7 +198,7 @@ Singleton { function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { root.addDebugInfo(qsTr("=== connectToNetworkWithPasswordCheck ===")); root.addDebugInfo(qsTr("SSID: %1, isSecure: %2").arg(ssid).arg(isSecure)); - + // For secure networks, try connecting without password first // If connection succeeds (saved password exists), we're done // If it fails with password error, callback will be called to show password dialog @@ -228,7 +228,7 @@ Singleton { disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); } } - + function forgetNetwork(ssid: string): void { // Delete the connection profile for this network // This will remove the saved password and connection settings @@ -240,7 +240,7 @@ Singleton { }, 500); } } - + function hasConnectionProfile(ssid: string): bool { // Check if a connection profile exists for this SSID // This is synchronous check - returns true if connection exists @@ -252,12 +252,12 @@ Singleton { // The actual check will be done asynchronously return false; } - + property list savedConnections: [] property list savedConnectionSsids: [] property var wifiConnectionQueue: [] property int currentSsidQueryIndex: 0 - + Process { id: listConnectionsProc command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] @@ -276,12 +276,12 @@ Singleton { } } } - + function parseConnectionList(output: string): void { const lines = output.trim().split("\n").filter(line => line.length > 0); const wifiConnections = []; const connections = []; - + // First pass: identify WiFi connections for (const line of lines) { const parts = line.split(":"); @@ -289,15 +289,15 @@ Singleton { const name = parts[0]; const type = parts[1]; connections.push(name); - + if (type === "802-11-wireless") { wifiConnections.push(name); } } } - + root.savedConnections = connections; - + // Second pass: get SSIDs for WiFi connections if (wifiConnections.length > 0) { root.wifiConnectionQueue = wifiConnections; @@ -310,10 +310,10 @@ Singleton { root.wifiConnectionQueue = []; } } - + Process { id: getSsidProc - + environment: ({ LANG: "C.UTF-8", LC_ALL: "C.UTF-8" @@ -332,7 +332,7 @@ Singleton { } } } - + function processSsidOutput(output: string): void { // Parse "802-11-wireless.ssid:SSID_NAME" format const lines = output.trim().split("\n"); @@ -351,11 +351,11 @@ Singleton { } } } - + // Query next connection queryNextSsid(); } - + function queryNextSsid(): void { if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; @@ -368,13 +368,13 @@ Singleton { root.currentSsidQueryIndex = 0; } } - + function hasSavedProfile(ssid: string): bool { if (!ssid || ssid.length === 0) { return false; } const ssidLower = ssid.toLowerCase().trim(); - + // If currently connected to this network, it definitely has a saved profile if (root.active && root.active.ssid) { const activeSsidLower = root.active.ssid.toLowerCase().trim(); @@ -382,21 +382,21 @@ Singleton { return true; } } - + // Check if SSID is in saved connections (case-insensitive comparison) - const hasSsid = root.savedConnectionSsids.some(savedSsid => + const hasSsid = root.savedConnectionSsids.some(savedSsid => savedSsid && savedSsid.toLowerCase().trim() === ssidLower ); - + if (hasSsid) { return true; } - + // Fallback: also check if connection name matches SSID (some connections use SSID as name) - const hasConnectionName = root.savedConnections.some(connName => + const hasConnectionName = root.savedConnections.some(connName => connName && connName.toLowerCase().trim() === ssidLower ); - + return hasConnectionName; } @@ -442,7 +442,7 @@ Singleton { if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { return ""; } - + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; const octets = [ (mask >>> 24) & 0xff, @@ -450,7 +450,7 @@ Singleton { (mask >>> 8) & 0xff, mask & 0xff ]; - + return octets.join("."); } @@ -510,7 +510,7 @@ Singleton { root.addDebugInfo(qsTr(" Pending SSID: %1").arg(root.pendingConnection.ssid)); root.addDebugInfo(qsTr(" Active SSID: %1").arg(root.active ? root.active.ssid : "None")); root.addDebugInfo(qsTr(" Connected: %1").arg(connected)); - + if (!connected && root.pendingConnection.callback) { // Connection didn't succeed after multiple checks, show password dialog root.addDebugInfo(qsTr("Connection failed - calling password dialog callback")); @@ -543,19 +543,19 @@ Singleton { repeat: true triggeredOnStart: false property int checkCount: 0 - + onRunningChanged: { if (running) { root.addDebugInfo(qsTr("Immediate check timer started (checks every 500ms)")); } } - + onTriggered: { if (root.pendingConnection) { checkCount++; const connected = root.active && root.active.ssid === root.pendingConnection.ssid; root.addDebugInfo(qsTr("Immediate check #%1: Connected=%2").arg(checkCount).arg(connected)); - + if (connected) { // Connection succeeded, stop timers and clear pending root.addDebugInfo(qsTr("Connection succeeded on check #%1!").arg(checkCount)); @@ -586,32 +586,32 @@ Singleton { onRunningChanged: { root.addDebugInfo(qsTr("Process running changed to: %1").arg(running)); } - + onStarted: { root.addDebugInfo(qsTr("Process started successfully")); } - + onExited: { root.addDebugInfo(qsTr("=== PROCESS EXITED ===")); root.addDebugInfo(qsTr("Exit code: %1").arg(exitCode)); root.addDebugInfo(qsTr("(Exit code 0 = success, non-zero = error)")); - + // Check if this was a "connection add" command - if so, we need to activate it - const wasConnectionAdd = connectProc.command && connectProc.command.length > 0 - && connectProc.command[0] === "nmcli" - && connectProc.command[1] === "connection" + const wasConnectionAdd = connectProc.command && connectProc.command.length > 0 + && connectProc.command[0] === "nmcli" + && connectProc.command[1] === "connection" && connectProc.command[2] === "add"; - + if (wasConnectionAdd && root.pendingConnection) { const ssid = root.pendingConnection.ssid; - + // Check for duplicate connection warning in stderr text const stderrText = connectProc.stderr ? connectProc.stderr.text : ""; const hasDuplicateWarning = stderrText && ( stderrText.includes("another connection with the name") || stderrText.includes("Reference the connection by its uuid") ); - + // Even with duplicate warning (or if connection already exists), we should try to activate it // Also try if exit code is non-zero but small (might be a warning, not a real error) if (exitCode === 0 || hasDuplicateWarning || (exitCode > 0 && exitCode < 10)) { @@ -622,10 +622,10 @@ Singleton { root.addDebugInfo(qsTr("Connection profile created successfully, now activating: %1").arg(ssid)); root.setConnectionStatus(qsTr("Activating connection...")); } - + // Update saved connections list listConnectionsProc.running = true; - + // Try to activate the connection by SSID (connection name) connectProc.command = ["nmcli", "connection", "up", ssid]; Qt.callLater(() => { @@ -644,7 +644,7 @@ Singleton { password = connectProc.command[pskIndex + 1]; } } - + if (password && password.length > 0) { root.addDebugInfo(qsTr("Using device wifi connect with password as fallback")); connectProc.command = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; @@ -655,10 +655,10 @@ Singleton { } } } - + // Refresh network list after connection attempt getNetworks.running = true; - + // Check if connection succeeded after a short delay (network list needs to update) if (root.pendingConnection) { if (exitCode === 0) { @@ -704,10 +704,10 @@ Singleton { root.addDebugInfo(qsTr("STDERR: %1").arg(line)); } } - + // Check for specific errors that indicate password is needed // Be careful not to match success messages - const needsPassword = (error.includes("Secrets were required") || + const needsPassword = (error.includes("Secrets were required") || error.includes("No secrets provided") || error.includes("802-11-wireless-security.psk") || (error.includes("password") && !error.includes("Connection activated")) || @@ -715,7 +715,7 @@ Singleton { (error.includes("802.11") && !error.includes("Connection activated"))) && !error.includes("Connection activated") && !error.includes("successfully"); - + if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { // Connection failed because password is needed - show dialog immediately connectionCheckTimer.stop(); @@ -784,7 +784,7 @@ Singleton { Process { id: deleteConnectionProc - + // Delete connection profile - refresh network list and saved connections after deletion onExited: { // Refresh network list and saved connections after deletion @@ -924,12 +924,12 @@ Singleton { onStreamFinished: { const output = text.trim(); root.ethernetDebugInfo = "Output received in onStreamFinished! Length: " + output.length + ", First 100 chars: " + output.substring(0, 100); - + if (!output || output.length === 0) { root.ethernetDebugInfo = "No output received (empty)"; return; } - + root.processEthernetOutput(output); } } @@ -942,7 +942,7 @@ Singleton { const lines = output.split("\n"); root.ethernetDebugInfo = "Processing " + lines.length + " lines"; - + const allDevices = lines.map(d => { const dev = d.replace(rep, PLACEHOLDER).split(":"); return { @@ -952,9 +952,9 @@ Singleton { connection: dev[3]?.replace(rep2, ":") ?? "" }; }); - + root.ethernetDebugInfo = "All devices: " + allDevices.length + ", Types: " + allDevices.map(d => d.type).join(", "); - + const ethernetOnly = allDevices.filter(d => d.type === "ethernet"); root.ethernetDebugInfo = "Ethernet devices found: " + ethernetOnly.length; @@ -975,7 +975,7 @@ Singleton { speed: "" }; }); - + root.ethernetDebugInfo = "Ethernet devices processed: " + ethernetDevices.length + ", First device: " + (ethernetDevices[0]?.interface || "none"); // Update the list - replace the entire array to ensure QML detects the change @@ -984,13 +984,13 @@ Singleton { for (let i = 0; i < ethernetDevices.length; i++) { newDevices.push(ethernetDevices[i]); } - + // Replace the entire list root.ethernetDevices = newDevices; - + // Force QML to detect the change by updating a property root.ethernetDeviceCount = ethernetDevices.length; - + // Force QML to re-evaluate the list by accessing it Qt.callLater(() => { const count = root.ethernetDevices.length; @@ -1130,7 +1130,7 @@ const line = lines[i]; // Find the connected wifi interface from device status const lines = output.split("\n"); let wifiInterface = ""; - + for (let i = 0; i < lines.length; i++) { const line = lines[i]; const parts = line.split(/\s+/); diff --git a/services/VPN.qml b/services/VPN.qml index 10e5e7e..412bda4 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -21,7 +21,7 @@ Singleton { const name = providerName; const iface = interfaceName; const defaults = getBuiltinDefaults(name, iface); - + if (isCustomProvider) { const custom = providerInput; return { @@ -31,7 +31,7 @@ Singleton { displayName: custom.displayName || defaults.displayName }; } - + return defaults; } @@ -62,7 +62,7 @@ Singleton { displayName: "Tailscale" } }; - + return builtins[name] || { connectCmd: [name, "up"], disconnectCmd: [name, "down"], diff --git a/utils/Icons.qml b/utils/Icons.qml index e946c4f..389eca3 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -194,13 +194,13 @@ Singleton { function getSpecialWsIcon(name: string): string { name = name.toLowerCase().slice("special:".length); - + for (const iconConfig of Config.bar.workspaces.specialWorkspaceIcons) { if (iconConfig.name === name) { return iconConfig.icon; } } - + if (name === "special") return "star"; if (name === "communication") -- cgit v1.2.3-freya From 468cf3d02aa1aabfbe565bdb93a436c62d8de792 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 14:47:18 -0500 Subject: readme: added showEthernet bool --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a2322eb..112a174 100644 --- a/README.md +++ b/README.md @@ -372,6 +372,7 @@ default, you must create it manually. "showAudio": false, "showBattery": true, "showBluetooth": true, + "showEthernet": true, "showKbLayout": false, "showMicrophone": false, "showNetwork": true, -- cgit v1.2.3-freya From a13f69531ff67bbf4f1e7d9f4e5d391090459fd8 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 14:50:11 -0500 Subject: drawers: reverted changes to c0ea060f --- modules/drawers/Interactions.qml | 46 ---------------------------------------- 1 file changed, 46 deletions(-) diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 2d0c115..9579b15 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -198,52 +198,6 @@ CustomMouseArea { utilitiesShortcutActive = false; } - // Show notifications panel on hover - // Try using inTopPanel first (works when panel is visible), fallback to corner detection only when panel is collapsed - const panelHeight = panels.notifications.height || panels.notifications.implicitHeight || 0; - const panelWidth = panels.notifications.width || panels.notifications.implicitWidth || Config.notifs.sizes.width; - const panelX = bar.implicitWidth + panels.notifications.x; - const isPanelCollapsed = panelHeight < 10; // Consider collapsed if height is very small - - let showNotifications = inTopPanel(panels.notifications, x, y); - - // Only use fallback corner detection when panel is collapsed - if (!showNotifications && isPanelCollapsed) { - // Use panel's actual width and position for fallback, with some padding - const cornerPadding = Config.border.rounding || 20; - showNotifications = x >= panelX - cornerPadding && - x <= panelX + panelWidth + cornerPadding && - y < Config.border.thickness + cornerPadding; - } - - // Check if mouse is over the clear all button area - // Button is positioned to the left of the notification panel - if (!showNotifications && panels.notifications.height > 0 && panels.clearAllButton && panels.clearAllButton.visible) { - const buttonX = bar.implicitWidth + panels.clearAllButton.x; - const buttonY = Config.border.thickness + panels.clearAllButton.y; - const buttonWidth = panels.clearAllButton.width; - const buttonHeight = panels.clearAllButton.height; - - const inButtonArea = x >= buttonX && - x <= buttonX + buttonWidth && - y >= buttonY && - y <= buttonY + buttonHeight; - - if (inButtonArea) { - showNotifications = true; - } - } - - // Show or hide notification panel based on hover - if (panels.notifications.content) { - if (showNotifications) { - panels.notifications.content.show(); - } else { - // Hide if not hovering over panel or button - panels.notifications.content.shouldShow = false; - } - } - // Show popouts on hover if (x < bar.implicitWidth) { bar.checkPopout(y); -- cgit v1.2.3-freya From 02d4feb3ab2b8cce239d5bd2c22593b5cf972ea1 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 14:59:03 -0500 Subject: drawers: reverted changes to c0ea060 --- modules/drawers/Drawers.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 5337917..2ba79a4 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -140,7 +140,6 @@ Variants { property bool dashboard property bool utilities property bool sidebar - property bool notifications Component.onCompleted: Visibilities.load(scope.modelData, this) } -- cgit v1.2.3-freya From 38c613a75e60f3b8a393356712117096c7e111c5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 15:08:01 -0500 Subject: controlcenter: wireless debug removal (preparing for rewrite) --- services/Network.qml | 209 +-------------------------------------------------- 1 file changed, 1 insertion(+), 208 deletions(-) diff --git a/services/Network.qml b/services/Network.qml index 7732a1c..2b79f12 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -24,38 +24,9 @@ Singleton { property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null property int ethernetDeviceCount: 0 - property string ethernetDebugInfo: "" property bool ethernetProcessRunning: false property var ethernetDeviceDetails: null property var wirelessDeviceDetails: null - property string connectionStatus: "" - property string connectionDebug: "" - - function clearConnectionStatus(): void { - connectionStatus = ""; - // Don't clear debug - keep it for reference - // connectionDebug = ""; - } - - function setConnectionStatus(status: string): void { - connectionStatus = status; - } - - function addDebugInfo(info: string): void { - const timestamp = new Date().toLocaleTimeString(); - const newInfo = "[" + timestamp + "] " + info; - // CRITICAL: Always append - NEVER replace - // Get current value - NEVER allow it to be empty/cleared - let current = connectionDebug; - if (!current || current === undefined || current === null) { - current = ""; - } - // ALWAYS append - never replace - // If current is empty, just use newInfo, otherwise append with newline - const updated = (current.length > 0) ? (current + "\n" + newInfo) : newInfo; - // CRITICAL: Only assign if we're appending, never replace - connectionDebug = updated; - } function enableWifi(enabled: bool): void { const cmd = enabled ? "on" : "off"; @@ -98,7 +69,6 @@ Singleton { if (existingConnection) { // Connection already exists - delete it first, then create new one with updated password - root.addDebugInfo(qsTr("Connection '%1' already exists, deleting it first...").arg(existingConnection)); deleteConnectionProc.exec(["nmcli", "connection", "delete", existingConnection]); // Wait a moment for deletion to complete, then create new connection Qt.callLater(() => { @@ -113,62 +83,26 @@ Singleton { } else { // Fallback to SSID if BSSID not available - use device wifi connect cmd = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; - root.setConnectionStatus(qsTr("Connecting to %1...").arg(ssid)); - root.addDebugInfo(qsTr("Using SSID only (no BSSID): %1").arg(ssid)); } } else { // Try to connect to existing connection first (will use saved password if available) cmd = ["nmcli", "device", "wifi", "connect", ssid]; - root.setConnectionStatus(qsTr("Connecting to %1 (using saved password)...").arg(ssid)); - root.addDebugInfo(qsTr("Using saved password for: %1").arg(ssid)); } - // Show the exact command being executed - const cmdStr = cmd.join(" "); - root.addDebugInfo(qsTr("=== COMMAND TO EXECUTE ===")); - root.addDebugInfo(qsTr("Command: %1").arg(cmdStr)); - root.addDebugInfo(qsTr("Command array: [%1]").arg(cmd.map((arg, i) => `"${arg}"`).join(", "))); - root.addDebugInfo(qsTr("Command array length: %1").arg(cmd.length)); - root.addDebugInfo(qsTr("===========================")); - // Set command and start process - root.addDebugInfo(qsTr("Setting command property...")); connectProc.command = cmd; - const setCmdStr = connectProc.command ? connectProc.command.join(" ") : "null"; - root.addDebugInfo(qsTr("Command property set, value: %1").arg(setCmdStr)); - root.addDebugInfo(qsTr("Command property verified: %1").arg(setCmdStr === cmdStr ? "Match" : "MISMATCH")); // If we're creating a connection profile, we need to activate it after creation const isConnectionAdd = cmd.length > 0 && cmd[0] === "nmcli" && cmd[1] === "connection" && cmd[2] === "add"; // Wait a moment before starting to ensure command is set Qt.callLater(() => { - root.addDebugInfo(qsTr("=== STARTING PROCESS ===")); - root.addDebugInfo(qsTr("Current running state: %1").arg(connectProc.running)); - root.addDebugInfo(qsTr("Command to run: %1").arg(connectProc.command ? connectProc.command.join(" ") : "NOT SET")); - root.addDebugInfo(qsTr("Is connection add command: %1").arg(isConnectionAdd)); connectProc.running = true; - root.addDebugInfo(qsTr("Process running set to: %1").arg(connectProc.running)); - root.addDebugInfo(qsTr("========================")); - - // Check if process actually started after a short delay - Qt.callLater(() => { - root.addDebugInfo(qsTr("Process status check (100ms later):")); - root.addDebugInfo(qsTr(" Running: %1").arg(connectProc.running)); - root.addDebugInfo(qsTr(" Command: %1").arg(connectProc.command ? connectProc.command.join(" ") : "null")); - if (!connectProc.running) { - root.addDebugInfo(qsTr("WARNING: Process did not start!")); - root.setConnectionStatus(qsTr("Error: Process failed to start")); - } - }, 100); }); // Start connection check timer if we have a callback if (callback) { - root.addDebugInfo(qsTr("Starting connection check timer (4 second interval)")); connectionCheckTimer.start(); - } else { - root.addDebugInfo(qsTr("No callback provided - not starting connection check timer")); } } @@ -183,10 +117,6 @@ Singleton { "802-11-wireless-security.key-mgmt", "wpa-psk", "802-11-wireless-security.psk", password]; - root.setConnectionStatus(qsTr("Connecting to %1 (BSSID: %2)...").arg(ssid).arg(bssidUpper)); - root.addDebugInfo(qsTr("Using BSSID: %1 for SSID: %2").arg(bssidUpper).arg(ssid)); - root.addDebugInfo(qsTr("Creating connection profile with password and key-mgmt")); - // Set command and start process connectProc.command = cmd; @@ -196,26 +126,19 @@ Singleton { } function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { - root.addDebugInfo(qsTr("=== connectToNetworkWithPasswordCheck ===")); - root.addDebugInfo(qsTr("SSID: %1, isSecure: %2").arg(ssid).arg(isSecure)); - // For secure networks, try connecting without password first // If connection succeeds (saved password exists), we're done // If it fails with password error, callback will be called to show password dialog if (isSecure) { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; - root.addDebugInfo(qsTr("Trying to connect without password (will use saved if available)")); // Try connecting without password - will use saved password if available connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); // Start timer to check if connection succeeded - root.addDebugInfo(qsTr("Starting connection check timer")); connectionCheckTimer.start(); } else { - root.addDebugInfo(qsTr("Network is not secure, connecting directly")); connectToNetwork(ssid, "", bssid, null); } - root.addDebugInfo(qsTr("=========================================")); } function disconnectFromNetwork(): void { @@ -503,17 +426,11 @@ Singleton { id: connectionCheckTimer interval: 4000 onTriggered: { - root.addDebugInfo(qsTr("=== CONNECTION CHECK TIMER (4s) ===")); if (root.pendingConnection) { const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - root.addDebugInfo(qsTr("Checking connection status...")); - root.addDebugInfo(qsTr(" Pending SSID: %1").arg(root.pendingConnection.ssid)); - root.addDebugInfo(qsTr(" Active SSID: %1").arg(root.active ? root.active.ssid : "None")); - root.addDebugInfo(qsTr(" Connected: %1").arg(connected)); if (!connected && root.pendingConnection.callback) { // Connection didn't succeed after multiple checks, show password dialog - root.addDebugInfo(qsTr("Connection failed - calling password dialog callback")); const pending = root.pendingConnection; root.pendingConnection = null; immediateCheckTimer.stop(); @@ -521,19 +438,11 @@ Singleton { pending.callback(); } else if (connected) { // Connection succeeded, clear pending - root.addDebugInfo(qsTr("Connection succeeded!")); - root.setConnectionStatus(qsTr("Connected successfully!")); root.pendingConnection = null; immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; - } else { - root.addDebugInfo(qsTr("Still connecting...")); - root.setConnectionStatus(qsTr("Still connecting...")); } - } else { - root.addDebugInfo(qsTr("No pending connection")); } - root.addDebugInfo(qsTr("================================")); } } @@ -544,36 +453,24 @@ Singleton { triggeredOnStart: false property int checkCount: 0 - onRunningChanged: { - if (running) { - root.addDebugInfo(qsTr("Immediate check timer started (checks every 500ms)")); - } - } - onTriggered: { if (root.pendingConnection) { checkCount++; const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - root.addDebugInfo(qsTr("Immediate check #%1: Connected=%2").arg(checkCount).arg(connected)); if (connected) { // Connection succeeded, stop timers and clear pending - root.addDebugInfo(qsTr("Connection succeeded on check #%1!").arg(checkCount)); - root.setConnectionStatus(qsTr("Connected successfully!")); connectionCheckTimer.stop(); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; root.pendingConnection = null; } else if (checkCount >= 6) { - root.addDebugInfo(qsTr("Checked %1 times (3 seconds) - connection taking longer").arg(checkCount)); - root.setConnectionStatus(qsTr("Connection taking longer than expected...")); // Checked 6 times (3 seconds total), connection likely failed // Stop immediate check, let the main timer handle it immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; } } else { - root.addDebugInfo(qsTr("Immediate check: No pending connection, stopping timer")); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; } @@ -583,18 +480,7 @@ Singleton { Process { id: connectProc - onRunningChanged: { - root.addDebugInfo(qsTr("Process running changed to: %1").arg(running)); - } - - onStarted: { - root.addDebugInfo(qsTr("Process started successfully")); - } - onExited: { - root.addDebugInfo(qsTr("=== PROCESS EXITED ===")); - root.addDebugInfo(qsTr("Exit code: %1").arg(exitCode)); - root.addDebugInfo(qsTr("(Exit code 0 = success, non-zero = error)")); // Check if this was a "connection add" command - if so, we need to activate it const wasConnectionAdd = connectProc.command && connectProc.command.length > 0 @@ -615,13 +501,6 @@ Singleton { // Even with duplicate warning (or if connection already exists), we should try to activate it // Also try if exit code is non-zero but small (might be a warning, not a real error) if (exitCode === 0 || hasDuplicateWarning || (exitCode > 0 && exitCode < 10)) { - if (hasDuplicateWarning) { - root.addDebugInfo(qsTr("Connection with name '%1' already exists (warning), will try to activate it").arg(ssid)); - root.setConnectionStatus(qsTr("Activating existing connection...")); - } else { - root.addDebugInfo(qsTr("Connection profile created successfully, now activating: %1").arg(ssid)); - root.setConnectionStatus(qsTr("Activating connection...")); - } // Update saved connections list listConnectionsProc.running = true; @@ -635,7 +514,6 @@ Singleton { return; } else { // Connection add failed - try using device wifi connect as fallback - root.addDebugInfo(qsTr("Connection add failed (exit code %1), trying device wifi connect as fallback").arg(exitCode)); // Extract password from the command if available let password = ""; if (connectProc.command) { @@ -646,7 +524,6 @@ Singleton { } if (password && password.length > 0) { - root.addDebugInfo(qsTr("Using device wifi connect with password as fallback")); connectProc.command = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; Qt.callLater(() => { connectProc.running = true; @@ -661,50 +538,18 @@ Singleton { // Check if connection succeeded after a short delay (network list needs to update) if (root.pendingConnection) { - if (exitCode === 0) { - // Process succeeded, start checking connection status - root.setConnectionStatus(qsTr("Connection command succeeded, verifying...")); - root.addDebugInfo(qsTr("Command succeeded, checking connection status...")); - root.addDebugInfo(qsTr("Starting immediate check timer (500ms intervals)")); - immediateCheckTimer.start(); - } else { - // Process failed, but wait a moment to see if connection still works - root.setConnectionStatus(qsTr("Connection command exited with code %1, checking status...").arg(exitCode)); - root.addDebugInfo(qsTr("Command exited with error code %1").arg(exitCode)); - root.addDebugInfo(qsTr("This usually means the command failed")); - root.addDebugInfo(qsTr("Checking connection status anyway...")); - root.addDebugInfo(qsTr("Starting immediate check timer (500ms intervals)")); - immediateCheckTimer.start(); - } - } else { - root.addDebugInfo(qsTr("No pending connection - not starting immediate check timer")); + immediateCheckTimer.start(); } - root.addDebugInfo(qsTr("======================")); } stdout: SplitParser { onRead: { getNetworks.running = true; - // Also log output for debugging - if (text && text.trim().length > 0) { - root.addDebugInfo(qsTr("STDOUT: %1").arg(text.trim())); - root.setConnectionStatus(qsTr("Status: %1").arg(text.trim())); - } } } stderr: StdioCollector { onStreamFinished: { const error = text.trim(); - root.addDebugInfo(qsTr("=== STDERR OUTPUT ===")); if (error && error.length > 0) { - // Split error into lines and add each one - const errorLines = error.split("\n"); - for (let i = 0; i < errorLines.length; i++) { - const line = errorLines[i].trim(); - if (line.length > 0) { - root.addDebugInfo(qsTr("STDERR: %1").arg(line)); - } - } - // Check for specific errors that indicate password is needed // Be careful not to match success messages const needsPassword = (error.includes("Secrets were required") || @@ -724,18 +569,10 @@ Singleton { root.pendingConnection = null; pending.callback(); } else if (error && error.length > 0 && !error.includes("Connection activated") && !error.includes("successfully")) { - // Log all errors (except success messages) - root.setConnectionStatus(qsTr("Error: %1").arg(errorLines[0] || error)); // Emit signal for UI to handle root.connectionFailed(root.pendingConnection ? root.pendingConnection.ssid : ""); - } else if (error && (error.includes("Connection activated") || error.includes("successfully"))) { - root.addDebugInfo(qsTr("Connection successful!")); - root.setConnectionStatus(qsTr("Connection successful!")); } - } else { - root.addDebugInfo(qsTr("STDERR: (empty)")); } - root.addDebugInfo(qsTr("====================")); } } } @@ -752,10 +589,6 @@ Singleton { } stderr: StdioCollector { onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { - console.warn("Network device disconnect error:", error); - } } } } @@ -774,7 +607,6 @@ Singleton { onStreamFinished: { const error = text.trim(); if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { - console.warn("Network connection disconnect error:", error); // If connection down failed, try device disconnect as fallback disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); } @@ -793,11 +625,6 @@ Singleton { } stderr: StdioCollector { onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0) { - // Log error but don't fail - connection might not exist - console.warn("Network connection delete error:", error); - } } } } @@ -896,26 +723,14 @@ Singleton { }) onRunningChanged: { root.ethernetProcessRunning = running; - if (!running) { - // Process finished, update debug info - Qt.callLater(() => { - if (root.ethernetDebugInfo === "" || root.ethernetDebugInfo.includes("Process exited")) { - root.ethernetDebugInfo = "Process finished, waiting for output..."; - } - }); - } } onExited: { Qt.callLater(() => { const outputLength = ethernetStdout.text ? ethernetStdout.text.length : 0; - root.ethernetDebugInfo = "Process exited with code: " + exitCode + ", output length: " + outputLength; if (outputLength > 0) { // Output was captured, process it const output = ethernetStdout.text.trim(); - root.ethernetDebugInfo = "Processing output from onExited, length: " + output.length + "\nOutput: " + output.substring(0, 200); root.processEthernetOutput(output); - } else { - root.ethernetDebugInfo = "No output captured in onExited"; } }); } @@ -923,10 +738,8 @@ Singleton { id: ethernetStdout onStreamFinished: { const output = text.trim(); - root.ethernetDebugInfo = "Output received in onStreamFinished! Length: " + output.length + ", First 100 chars: " + output.substring(0, 100); if (!output || output.length === 0) { - root.ethernetDebugInfo = "No output received (empty)"; return; } @@ -941,7 +754,6 @@ Singleton { const rep2 = new RegExp(PLACEHOLDER, "g"); const lines = output.split("\n"); - root.ethernetDebugInfo = "Processing " + lines.length + " lines"; const allDevices = lines.map(d => { const dev = d.replace(rep, PLACEHOLDER).split(":"); @@ -953,10 +765,7 @@ Singleton { }; }); - root.ethernetDebugInfo = "All devices: " + allDevices.length + ", Types: " + allDevices.map(d => d.type).join(", "); - const ethernetOnly = allDevices.filter(d => d.type === "ethernet"); - root.ethernetDebugInfo = "Ethernet devices found: " + ethernetOnly.length; const ethernetDevices = ethernetOnly.map(d => { const state = d.state || ""; @@ -976,8 +785,6 @@ Singleton { }; }); - root.ethernetDebugInfo = "Ethernet devices processed: " + ethernetDevices.length + ", First device: " + (ethernetDevices[0]?.interface || "none"); - // Update the list - replace the entire array to ensure QML detects the change // Create a new array and assign it to the property const newDevices = []; @@ -990,12 +797,6 @@ Singleton { // Force QML to detect the change by updating a property root.ethernetDeviceCount = ethernetDevices.length; - - // Force QML to re-evaluate the list by accessing it - Qt.callLater(() => { - const count = root.ethernetDevices.length; - root.ethernetDebugInfo = "Final: Found " + ethernetDevices.length + " devices, List length: " + count + ", Parsed all: " + allDevices.length + ", Output length: " + output.length; - }); } @@ -1017,10 +818,6 @@ Singleton { } stderr: StdioCollector { onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0 && !error.includes("successfully") && !error.includes("Connection activated")) { - console.warn("Ethernet connection error:", error); - } } } } @@ -1040,10 +837,6 @@ Singleton { } stderr: StdioCollector { onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { - console.warn("Ethernet disconnection error:", error); - } } } } -- cgit v1.2.3-freya From baa10d6ccfe8ad03a8832c4a43179c21020997ab Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 16:56:49 -0500 Subject: controlcenter: created dev panel for wireless testing --- modules/controlcenter/dev/DevControlCenter.qml | 94 ++++++++++++ modules/controlcenter/dev/DevDebugPane.qml | 170 ++++++++++++++++++++++ modules/controlcenter/dev/DevNavRail.qml | 194 +++++++++++++++++++++++++ modules/controlcenter/dev/DevPanes.qml | 70 +++++++++ modules/controlcenter/dev/DevSession.qml | 23 +++ modules/controlcenter/dev/DevWindowFactory.qml | 62 ++++++++ modules/controlcenter/dev/DevWindowTitle.qml | 53 +++++++ modules/controlcenter/dev/DevWirelessPane.qml | 68 +++++++++ modules/utilities/cards/Toggles.qml | 13 ++ 9 files changed, 747 insertions(+) create mode 100644 modules/controlcenter/dev/DevControlCenter.qml create mode 100644 modules/controlcenter/dev/DevDebugPane.qml create mode 100644 modules/controlcenter/dev/DevNavRail.qml create mode 100644 modules/controlcenter/dev/DevPanes.qml create mode 100644 modules/controlcenter/dev/DevSession.qml create mode 100644 modules/controlcenter/dev/DevWindowFactory.qml create mode 100644 modules/controlcenter/dev/DevWindowTitle.qml create mode 100644 modules/controlcenter/dev/DevWirelessPane.qml diff --git a/modules/controlcenter/dev/DevControlCenter.qml b/modules/controlcenter/dev/DevControlCenter.qml new file mode 100644 index 0000000..29592ca --- /dev/null +++ b/modules/controlcenter/dev/DevControlCenter.qml @@ -0,0 +1,94 @@ +pragma ComponentBehavior: Bound + +import "." +import ".." +import qs.components +import qs.components.controls +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property ShellScreen screen + readonly property int rounding: floating ? 0 : Appearance.rounding.normal + + property alias floating: session.floating + property alias active: session.active + property alias navExpanded: session.navExpanded + + readonly property DevSession session: DevSession { + id: session + + root: root + } + + function close(): void { + } + + implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio + implicitHeight: screen.height * Config.controlCenter.sizes.heightMult + + GridLayout { + anchors.fill: parent + + rowSpacing: 0 + columnSpacing: 0 + rows: root.floating ? 2 : 1 + columns: 2 + + Loader { + Layout.fillWidth: true + Layout.columnSpan: 2 + + asynchronous: true + active: root.floating + visible: active + + sourceComponent: DevWindowTitle { + screen: root.screen + session: root.session + } + } + + StyledRect { + Layout.fillHeight: true + + topLeftRadius: root.rounding + bottomLeftRadius: root.rounding + implicitWidth: navRail.implicitWidth + color: Colours.tPalette.m3surfaceContainer + + CustomMouseArea { + anchors.fill: parent + + function onWheel(event: WheelEvent): void { + if (event.angleDelta.y < 0) + root.session.activeIndex = Math.min(root.session.activeIndex + 1, root.session.panes.length - 1); + else if (event.angleDelta.y > 0) + root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0); + } + } + + DevNavRail { + id: navRail + + screen: root.screen + session: root.session + } + } + + DevPanes { + Layout.fillWidth: true + Layout.fillHeight: true + + topRightRadius: root.rounding + bottomRightRadius: root.rounding + session: root.session + } + } +} + diff --git a/modules/controlcenter/dev/DevDebugPane.qml b/modules/controlcenter/dev/DevDebugPane.qml new file mode 100644 index 0000000..88d6542 --- /dev/null +++ b/modules/controlcenter/dev/DevDebugPane.qml @@ -0,0 +1,170 @@ +pragma ComponentBehavior: Bound + +import "." +import ".." +import qs.components +import qs.components.controls +import qs.components.containers +import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property DevSession session + + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Debug Panel") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + // Action Buttons Section + StyledRect { + Layout.fillWidth: true + implicitHeight: buttonsLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: buttonsLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Actions") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + Flow { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + TextButton { + text: qsTr("Clear Log") + onClicked: { + debugOutput.text = ""; + appendLog("Debug log cleared"); + } + } + + TextButton { + text: qsTr("Test Action") + onClicked: { + appendLog("Test action executed at " + new Date().toLocaleTimeString()); + } + } + + TextButton { + text: qsTr("Log Network State") + onClicked: { + appendLog("Network state:"); + appendLog(" Active: " + (root.session.network.active ? "Yes" : "No")); + } + } + } + } + } + + // Debug Output Section + StyledRect { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + + StyledText { + text: qsTr("Debug Output") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Copy") + onClicked: { + debugOutput.selectAll(); + debugOutput.copy(); + debugOutput.deselect(); + appendLog("Output copied to clipboard"); + } + } + } + + StyledFlickable { + id: flickable + + Layout.fillWidth: true + Layout.fillHeight: true + flickableDirection: Flickable.VerticalFlick + contentHeight: debugOutput.implicitHeight + + TextEdit { + id: debugOutput + + width: flickable.width + readOnly: true + wrapMode: TextEdit.Wrap + font.family: Appearance.font.family.mono + font.pointSize: Appearance.font.size.smaller + renderType: TextEdit.NativeRendering + textFormat: TextEdit.PlainText + color: "#ffb0ca" // Use primary color - will be set programmatically + + Component.onCompleted: { + color = Colours.palette.m3primary; + appendLog("Debug panel initialized"); + } + + onTextChanged: { + // Ensure color stays set when text changes + color = Colours.palette.m3primary; + if (flickable.contentHeight > flickable.height) { + flickable.contentY = flickable.contentHeight - flickable.height; + } + } + } + } + + StyledScrollBar { + flickable: flickable + policy: ScrollBar.AlwaysOn + } + } + } + } + + function appendLog(message: string): void { + const timestamp = new Date().toLocaleTimeString(); + debugOutput.text += `[${timestamp}] ${message}\n`; + } + + function log(message: string): void { + appendLog(message); + } +} + diff --git a/modules/controlcenter/dev/DevNavRail.qml b/modules/controlcenter/dev/DevNavRail.qml new file mode 100644 index 0000000..d2f2d57 --- /dev/null +++ b/modules/controlcenter/dev/DevNavRail.qml @@ -0,0 +1,194 @@ +pragma ComponentBehavior: Bound + +import "." +import ".." +import qs.components +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property ShellScreen screen + required property DevSession session + + implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4 + implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.padding.larger * 2 + spacing: Appearance.spacing.normal + + states: State { + name: "expanded" + when: root.session.navExpanded + + PropertyChanges { + layout.spacing: Appearance.spacing.small + menuIcon.opacity: 0 + menuIconExpanded.opacity: 1 + menuIcon.rotation: 180 + menuIconExpanded.rotation: 0 + } + } + + transitions: Transition { + Anim { + properties: "spacing,opacity,rotation" + } + } + + Item { + id: menuBtn + + Layout.topMargin: Appearance.spacing.large + implicitWidth: menuIcon.implicitWidth + menuIcon.anchors.leftMargin * 2 + implicitHeight: menuIcon.implicitHeight + Appearance.padding.normal * 2 + + StateLayer { + radius: Appearance.rounding.small + + function onClicked(): void { + root.session.navExpanded = !root.session.navExpanded; + } + } + + MaterialIcon { + id: menuIcon + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.padding.large + + text: "menu" + font.pointSize: Appearance.font.size.large + } + + MaterialIcon { + id: menuIconExpanded + + anchors.fill: menuIcon + text: "menu_open" + font.pointSize: menuIcon.font.pointSize + opacity: 0 + rotation: -180 + } + } + + NavItem { + Layout.topMargin: Appearance.spacing.large * 2 + icon: "wifi" + label: "wireless" + } + + NavItem { + icon: "bug_report" + label: "debug" + } + } + + component NavItem: Item { + id: item + + required property string icon + required property string label + readonly property bool active: root.session.active === label + + implicitWidth: background.implicitWidth + implicitHeight: background.implicitHeight + smallLabel.implicitHeight + smallLabel.anchors.topMargin + + states: State { + name: "expanded" + when: root.session.navExpanded + + PropertyChanges { + expandedLabel.opacity: 1 + smallLabel.opacity: 0 + background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth + background.implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + item.implicitHeight: background.implicitHeight + } + } + + transitions: Transition { + Anim { + property: "opacity" + duration: Appearance.anim.durations.small + } + + Anim { + properties: "implicitWidth,implicitHeight" + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + StyledRect { + id: background + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3secondaryContainer, item.active ? 1 : 0) + + implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + implicitHeight: icon.implicitHeight + Appearance.padding.small + + StateLayer { + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + + function onClicked(): void { + root.session.active = item.label; + } + } + + MaterialIcon { + id: icon + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.padding.large + + text: item.icon + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.large + fill: item.active ? 1 : 0 + + Behavior on fill { + Anim {} + } + } + + StyledText { + id: expandedLabel + + anchors.left: icon.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.spacing.normal + + opacity: 0 + text: item.label + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + font.capitalization: Font.Capitalize + } + + StyledText { + id: smallLabel + + anchors.horizontalCenter: icon.horizontalCenter + anchors.top: icon.bottom + anchors.topMargin: Appearance.spacing.small / 2 + + text: item.label + font.pointSize: Appearance.font.size.small + font.capitalization: Font.Capitalize + } + } + } +} + diff --git a/modules/controlcenter/dev/DevPanes.qml b/modules/controlcenter/dev/DevPanes.qml new file mode 100644 index 0000000..6b5ce06 --- /dev/null +++ b/modules/controlcenter/dev/DevPanes.qml @@ -0,0 +1,70 @@ +pragma ComponentBehavior: Bound + +import "." +import ".." +import qs.components +import qs.services +import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +ClippingRectangle { + id: root + + required property DevSession session + + color: "transparent" + + ColumnLayout { + id: layout + + spacing: 0 + y: -root.session.activeIndex * root.height + + Pane { + index: 0 + sourceComponent: DevWirelessPane { + session: root.session + } + } + + Pane { + index: 1 + sourceComponent: DevDebugPane { + session: root.session + } + } + + Behavior on y { + Anim {} + } + } + + component Pane: Item { + id: pane + + required property int index + property alias sourceComponent: loader.sourceComponent + + implicitWidth: root.width + implicitHeight: root.height + + Loader { + id: loader + + anchors.fill: parent + clip: true + asynchronous: true + active: { + if (root.session.activeIndex === pane.index) + return true; + + const ly = -layout.y; + const ty = pane.index * root.height; + return ly + root.height > ty && ly < ty + root.height; + } + } + } +} + diff --git a/modules/controlcenter/dev/DevSession.qml b/modules/controlcenter/dev/DevSession.qml new file mode 100644 index 0000000..d911386 --- /dev/null +++ b/modules/controlcenter/dev/DevSession.qml @@ -0,0 +1,23 @@ +import QtQuick + +QtObject { + readonly property list panes: ["wireless", "debug"] + + required property var root + property bool floating: false + property string active: panes[0] + property int activeIndex: 0 + property bool navExpanded: false + + component Network: QtObject { + property var active + property bool showPasswordDialog: false + property var pendingNetwork + } + + readonly property Network network: Network {} + + onActiveChanged: activeIndex = panes.indexOf(active) + onActiveIndexChanged: active = panes[activeIndex] +} + diff --git a/modules/controlcenter/dev/DevWindowFactory.qml b/modules/controlcenter/dev/DevWindowFactory.qml new file mode 100644 index 0000000..5682588 --- /dev/null +++ b/modules/controlcenter/dev/DevWindowFactory.qml @@ -0,0 +1,62 @@ +pragma Singleton + +import "." +import qs.components +import qs.services +import Quickshell +import QtQuick + +Singleton { + id: root + + function create(parent: Item, props: var): void { + devControlCenter.createObject(parent ?? dummy, props); + } + + QtObject { + id: dummy + } + + Component { + id: devControlCenter + + FloatingWindow { + id: win + + property alias active: cc.active + property alias navExpanded: cc.navExpanded + + color: Colours.tPalette.m3surface + + onVisibleChanged: { + if (!visible) + destroy(); + } + + minimumSize.width: 1000 + minimumSize.height: 600 + + implicitWidth: cc.implicitWidth + implicitHeight: cc.implicitHeight + + title: qsTr("Dev Panel - Wireless") + + DevControlCenter { + id: cc + + anchors.fill: parent + screen: win.screen + floating: true + + function close(): void { + win.destroy(); + } + } + + Behavior on color { + CAnim {} + } + } + } +} + diff --git a/modules/controlcenter/dev/DevWindowTitle.qml b/modules/controlcenter/dev/DevWindowTitle.qml new file mode 100644 index 0000000..9395532 --- /dev/null +++ b/modules/controlcenter/dev/DevWindowTitle.qml @@ -0,0 +1,53 @@ +import "." +import qs.components +import qs.services +import qs.config +import Quickshell +import QtQuick + +StyledRect { + id: root + + required property ShellScreen screen + required property DevSession session + + implicitHeight: text.implicitHeight + Appearance.padding.normal + color: Colours.tPalette.m3surfaceContainer + + StyledText { + id: text + + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + + text: qsTr("Dev Panel - %1").arg(root.session.active.slice(0, 1).toUpperCase() + root.session.active.slice(1)) + font.capitalization: Font.Capitalize + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Appearance.padding.normal + + implicitWidth: implicitHeight + implicitHeight: closeIcon.implicitHeight + Appearance.padding.small + + StateLayer { + radius: Appearance.rounding.full + + function onClicked(): void { + QsWindow.window.destroy(); + } + } + + MaterialIcon { + id: closeIcon + + anchors.centerIn: parent + text: "close" + } + } +} + diff --git a/modules/controlcenter/dev/DevWirelessPane.qml b/modules/controlcenter/dev/DevWirelessPane.qml new file mode 100644 index 0000000..feb0ce7 --- /dev/null +++ b/modules/controlcenter/dev/DevWirelessPane.qml @@ -0,0 +1,68 @@ +pragma ComponentBehavior: Bound + +import "." +import ".." +import qs.components +import qs.components.effects +import qs.components.containers +import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property DevSession session + + anchors.fill: parent + + spacing: 0 + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true + + // Blank placeholder for wireless list + Item { + 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 + } + + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ClippingRectangle { + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + + radius: rightBorder.innerRadius + color: "transparent" + + // Blank placeholder for settings/details area + Item { + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + } + } + + InnerBorder { + id: rightBorder + + leftThickness: Appearance.padding.normal / 2 + } + } +} + diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 3d18e72..ccf1c7d 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -3,6 +3,7 @@ import qs.components.controls import qs.services import qs.config import qs.modules.controlcenter +import "../../controlcenter/dev" import Quickshell import Quickshell.Bluetooth import QtQuick @@ -92,6 +93,18 @@ StyledRect { visible: VPN.enabled onClicked: VPN.toggle() } + + Toggle { + icon: "bug_report" + inactiveOnColour: Colours.palette.m3onSurfaceVariant + toggle: false + onClicked: { + root.visibilities.utilities = false; + DevWindowFactory.create(null, { + screen: QsWindow.window?.screen ?? null + }); + } + } } } -- cgit v1.2.3-freya From 617e686238d3c7155112196043f0883ccf6a7012 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 18:59:48 -0500 Subject: service: Nmcli.qml --- modules/controlcenter/dev/DevDebugPane.qml | 1863 +++++++++++++++++++++++++++- plan.md | 137 ++ services/Nmcli.qml | 1246 +++++++++++++++++++ 3 files changed, 3219 insertions(+), 27 deletions(-) create mode 100644 plan.md create mode 100644 services/Nmcli.qml diff --git a/modules/controlcenter/dev/DevDebugPane.qml b/modules/controlcenter/dev/DevDebugPane.qml index 88d6542..1150f35 100644 --- a/modules/controlcenter/dev/DevDebugPane.qml +++ b/modules/controlcenter/dev/DevDebugPane.qml @@ -5,7 +5,9 @@ import ".." import qs.components import qs.components.controls import qs.components.containers +import qs.components.effects import qs.config +import qs.services import Quickshell import Quickshell.Widgets import QtQuick @@ -18,61 +20,1854 @@ Item { anchors.fill: parent - ColumnLayout { + // Track last failed connection + property string lastFailedSsid: "" + + // Connect to connection failure signal + Connections { + target: Nmcli + function onConnectionFailed(ssid: string) { + root.lastFailedSsid = ssid; + appendLog("Connection failed signal received for: " + ssid); + } + } + + StyledFlickable { + id: flickable + anchors.fill: parent anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + flickableDirection: Flickable.VerticalFlick + contentWidth: width + contentHeight: contentLayout.implicitHeight + + StyledScrollBar.vertical: StyledScrollBar { + flickable: flickable + } + + ColumnLayout { + id: contentLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Debug Panel") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + // Action Buttons Section + StyledRect { + Layout.fillWidth: true + implicitHeight: buttonsLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: buttonsLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Actions") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + Flow { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + TextButton { + text: qsTr("Clear Log") + onClicked: { + debugOutput.text = ""; + appendLog("Debug log cleared"); + } + } + + TextButton { + text: qsTr("Test Action") + onClicked: { + appendLog("Test action executed at " + new Date().toLocaleTimeString()); + } + } + + TextButton { + text: qsTr("Log Network State") + onClicked: { + appendLog("Network state:"); + appendLog(" Active: " + (root.session.network.active ? "Yes" : "No")); + } + } + + TextButton { + text: qsTr("Get Device Status") + onClicked: { + appendLog("Getting device status..."); + try { + Nmcli.getDeviceStatus((output) => { + if (!output) { + appendLog(" Error: No output received"); + return; + } + appendLog("Device Status:"); + const lines = output.trim().split("\n"); + if (lines.length === 0 || (lines.length === 1 && lines[0].length === 0)) { + appendLog(" No devices found"); + } else { + for (const line of lines) { + if (line.length > 0) { + appendLog(" " + line); + } + } + } + }); + } catch (e) { + appendLog("Error: " + e); + } + } + } + + TextButton { + text: qsTr("Get Wireless Interfaces") + onClicked: { + appendLog("Getting wireless interfaces..."); + Nmcli.getWirelessInterfaces((interfaces) => { + appendLog("Wireless Interfaces: " + interfaces.length); + for (const iface of interfaces) { + appendLog(` ${iface.device}: ${iface.state} (${iface.connection})`); + } + }); + } + } + + TextButton { + text: qsTr("Get Ethernet Interfaces") + onClicked: { + appendLog("Getting ethernet interfaces..."); + Nmcli.getEthernetInterfaces((interfaces) => { + appendLog("Ethernet Interfaces: " + interfaces.length); + for (const iface of interfaces) { + appendLog(` ${iface.device}: ${iface.state} (${iface.connection})`); + } + }); + } + } + + TextButton { + text: qsTr("Refresh Status") + onClicked: { + appendLog("Refreshing connection status..."); + Nmcli.refreshStatus((status) => { + appendLog("Connection Status:"); + appendLog(" Connected: " + (status.connected ? "Yes" : "No")); + appendLog(" Interface: " + (status.interface || "None")); + appendLog(" Connection: " + (status.connection || "None")); + }); + } + } + + TextButton { + text: qsTr("Check Interface") + onClicked: { + appendLog("Checking interface connection status..."); + // Check first wireless interface if available + if (Nmcli.wirelessInterfaces.length > 0) { + const iface = Nmcli.wirelessInterfaces[0].device; + appendLog("Checking: " + iface); + Nmcli.isInterfaceConnected(iface, (connected) => { + appendLog(` ${iface}: ${connected ? "Connected" : "Disconnected"}`); + }); + } else { + appendLog("No wireless interfaces found"); + } + } + } + } + } + } + + // WiFi Radio Control Section + StyledRect { + Layout.fillWidth: true + implicitHeight: wifiRadioLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: wifiRadioLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("WiFi Radio Control") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Status: ") + (Nmcli.wifiEnabled ? qsTr("Enabled") : qsTr("Disabled")) + color: Nmcli.wifiEnabled ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Toggle WiFi") + onClicked: { + appendLog("Toggling WiFi radio..."); + Nmcli.toggleWifi((result) => { + if (result.success) { + appendLog("WiFi radio toggled: " + (Nmcli.wifiEnabled ? "Enabled" : "Disabled")); + } else { + appendLog("Failed to toggle WiFi: " + (result.error || "Unknown error")); + } + }); + } + } + + TextButton { + text: qsTr("Enable") + onClicked: { + appendLog("Enabling WiFi radio..."); + Nmcli.enableWifi(true, (result) => { + if (result.success) { + appendLog("WiFi radio enabled"); + } else { + appendLog("Failed to enable WiFi: " + (result.error || "Unknown error")); + } + }); + } + } + + TextButton { + text: qsTr("Disable") + onClicked: { + appendLog("Disabling WiFi radio..."); + Nmcli.enableWifi(false, (result) => { + if (result.success) { + appendLog("WiFi radio disabled"); + } else { + appendLog("Failed to disable WiFi: " + (result.error || "Unknown error")); + } + }); + } + } + + TextButton { + text: qsTr("Check Status") + onClicked: { + appendLog("Checking WiFi radio status..."); + Nmcli.getWifiStatus((enabled) => { + appendLog("WiFi radio status: " + (enabled ? "Enabled" : "Disabled")); + }); + } + } + } + } + } + + // Network List Management Section + StyledRect { + Layout.fillWidth: true + implicitHeight: networkListLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: networkListLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Network List Management") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Networks: %1").arg(Nmcli.networks.length) + } + + StyledText { + visible: Nmcli.active + text: qsTr("Active: %1").arg(Nmcli.active.ssid) + color: Colours.palette.m3primary + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh Networks") + onClicked: { + appendLog("Refreshing network list..."); + Nmcli.getNetworks((networks) => { + appendLog("Found " + networks.length + " networks"); + if (Nmcli.active) { + appendLog("Active network: " + Nmcli.active.ssid + " (Signal: " + Nmcli.active.strength + "%, Security: " + (Nmcli.active.isSecure ? Nmcli.active.security : "Open") + ")"); + } else { + appendLog("No active network"); + } + }); + } + } + + TextButton { + text: qsTr("List All Networks") + onClicked: { + appendLog("Network list:"); + if (Nmcli.networks.length === 0) { + appendLog(" No networks found"); + } else { + for (let i = 0; i < Nmcli.networks.length; i++) { + const net = Nmcli.networks[i]; + const activeMark = net.active ? " [ACTIVE]" : ""; + appendLog(` ${i + 1}. ${net.ssid}${activeMark}`); + appendLog(` Signal: ${net.strength}%, Freq: ${net.frequency}MHz, Security: ${net.isSecure ? net.security : "Open"}`); + if (net.bssid) { + appendLog(` BSSID: ${net.bssid}`); + } + } + } + } + } + } + } + } + + // Interface Selector Section (for future features) + Item { + Layout.fillWidth: true + implicitHeight: interfaceSelectorContainer.implicitHeight + z: 10 // Ensure dropdown menu appears above other elements + + StyledRect { + id: interfaceSelectorContainer + + anchors.fill: parent + implicitHeight: interfaceSelectorLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: interfaceSelectorLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Interface Selector") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + SplitButton { + id: interfaceSelector + + type: SplitButton.Tonal + fallbackText: qsTr("Select Interface") + fallbackIcon: "settings_ethernet" + menuItems: interfaceList.instances + menuOnTop: true // Position menu above button to avoid being covered + + property string selectedInterface: "" + + menu.onItemSelected: (item) => { + interfaceSelector.selectedInterface = item.modelData.device; + appendLog("Selected interface: " + item.modelData.device + " (" + item.modelData.type + ")"); + } + + Variants { + id: interfaceList + + model: interfaceSelector.interfaces + + MenuItem { + required property var modelData + + text: modelData.device + " (" + modelData.type + ")" + icon: modelData.type === "wifi" ? "wifi" : "settings_ethernet" + } + } + + property list interfaces: [] + + function refreshInterfaces(): void { + appendLog("Refreshing interface list..."); + Nmcli.getAllInterfaces((interfaces) => { + interfaceSelector.interfaces = interfaces; + if (interfaces.length > 0) { + // Wait for Variants to create instances, then set active + Qt.callLater(() => { + if (interfaceList.instances.length > 0) { + interfaceSelector.active = interfaceList.instances[0]; + interfaceSelector.selectedInterface = interfaces[0].device; + } + }); + appendLog("Found " + interfaces.length + " interfaces"); + } else { + interfaceSelector.selectedInterface = ""; + appendLog("No interfaces found"); + } + }); + } + + Component.onCompleted: { + // Ensure menu appears above other elements + menu.z = 100; + } + } + + TextButton { + text: qsTr("Refresh") + onClicked: { + interfaceSelector.refreshInterfaces(); + } + } + + TextButton { + text: qsTr("Up") + enabled: interfaceSelector.selectedInterface.length > 0 + onClicked: { + if (interfaceSelector.selectedInterface) { + appendLog("Bringing interface up: " + interfaceSelector.selectedInterface); + Nmcli.bringInterfaceUp(interfaceSelector.selectedInterface, (result) => { + if (result.success) { + appendLog("Interface up: Success"); + } else { + appendLog("Interface up: Failed (exit code: " + result.exitCode + ")"); + if (result.error && result.error.length > 0) { + appendLog("Error: " + result.error); + } + } + // Refresh interface list after bringing up + Qt.callLater(() => { + interfaceSelector.refreshInterfaces(); + }, 500); + }); + } + } + } + + TextButton { + text: qsTr("Down") + enabled: interfaceSelector.selectedInterface.length > 0 + onClicked: { + if (interfaceSelector.selectedInterface) { + appendLog("Bringing interface down: " + interfaceSelector.selectedInterface); + Nmcli.bringInterfaceDown(interfaceSelector.selectedInterface, (result) => { + if (result.success) { + appendLog("Interface down: Success"); + } else { + appendLog("Interface down: Failed (exit code: " + result.exitCode + ")"); + if (result.error && result.error.length > 0) { + appendLog("Error: " + result.error); + } + } + // Refresh interface list after bringing down + Qt.callLater(() => { + interfaceSelector.refreshInterfaces(); + }, 500); + }); + } + } + } + } + } + } + } + + // Wireless SSID Selector Section + Item { + Layout.fillWidth: true + implicitHeight: ssidSelectorContainer.implicitHeight + z: 10 // Ensure dropdown menu appears above other elements + + StyledRect { + id: ssidSelectorContainer + + anchors.fill: parent + implicitHeight: ssidSelectorLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: ssidSelectorLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Wireless SSID Selector") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + SplitButton { + id: ssidSelector + + type: SplitButton.Tonal + fallbackText: qsTr("Select SSID") + fallbackIcon: "wifi" + menuItems: ssidList.instances + menuOnTop: true + + property string selectedSSID: "" + + menu.onItemSelected: (item) => { + ssidSelector.selectedSSID = item.modelData.ssid; + appendLog("Selected SSID: " + item.modelData.ssid + " (Signal: " + item.modelData.signal + ", Security: " + item.modelData.security + ")"); + } + + Component.onCompleted: { + // Ensure menu appears above other elements + menu.z = 100; + } + + Variants { + id: ssidList + + model: ssidSelector.ssids + + MenuItem { + required property var modelData + + text: modelData.ssid + (modelData.signal ? " (" + modelData.signal + "%)" : "") + icon: "wifi" + } + } + + property list ssids: [] + + function scanForSSIDs(): void { + appendLog("Scanning for wireless networks..."); + // Use first wireless interface if available, or let nmcli choose + let iface = ""; + if (interfaceSelector.selectedInterface) { + // Check if selected interface is wireless + for (const i of interfaceSelector.interfaces) { + if (i.device === interfaceSelector.selectedInterface && i.type === "wifi") { + iface = interfaceSelector.selectedInterface; + break; + } + } + } + + // If no wireless interface selected, use first available + if (!iface && Nmcli.wirelessInterfaces.length > 0) { + iface = Nmcli.wirelessInterfaces[0].device; + } + + Nmcli.scanWirelessNetworks(iface, (scanResult) => { + if (scanResult.success) { + appendLog("Scan completed, fetching SSID list..."); + // Wait a moment for scan results to be available + Qt.callLater(() => { + Nmcli.getWirelessSSIDs(iface, (ssids) => { + ssidSelector.ssids = ssids; + if (ssids.length > 0) { + Qt.callLater(() => { + if (ssidList.instances.length > 0) { + ssidSelector.active = ssidList.instances[0]; + ssidSelector.selectedSSID = ssids[0].ssid; + } + }); + appendLog("Found " + ssids.length + " SSIDs"); + } else { + appendLog("No SSIDs found"); + } + }); + }, 1000); + } else { + appendLog("Scan failed: " + (scanResult.error || "Unknown error")); + } + }); + } + } + + TextButton { + text: qsTr("Scan") + onClicked: { + ssidSelector.scanForSSIDs(); + } + } + } + } + } + } + + // Wireless Connection Test Section + StyledRect { + Layout.fillWidth: true + implicitHeight: connectionTestLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: connectionTestLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Wireless Connection Test") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("SSID: %1").arg(ssidSelector.selectedSSID || "None selected") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Connect (No Password)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + appendLog("Connecting to: " + ssidSelector.selectedSSID + " (no password)"); + // Find the network to get BSSID + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + const bssid = network ? network.bssid : ""; + Nmcli.connectWireless(ssidSelector.selectedSSID, "", bssid, (result) => { + if (result.success) { + appendLog("Connection succeeded!"); + // Refresh network list after connection + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else { + appendLog("Connection failed: " + (result.error || "Unknown error")); + // Refresh network list anyway to check status + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } + }); + appendLog("Connection initiated, tracking pending connection..."); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Password:") + } + + Item { + Layout.fillWidth: true + implicitHeight: passwordField.implicitHeight + Appearance.padding.small * 2 + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + border.width: passwordField.activeFocus ? 2 : 1 + border.color: passwordField.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline + + Behavior on border.color { + CAnim {} + } + } + + StyledTextField { + id: passwordField + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + echoMode: TextField.Password + placeholderText: qsTr("Enter password") + + Keys.onReturnPressed: { + if (connectWithPasswordButton.enabled) { + connectWithPasswordButton.clicked(); + } + } + Keys.onEnterPressed: { + if (connectWithPasswordButton.enabled) { + connectWithPasswordButton.clicked(); + } + } + } + } + + TextButton { + id: connectWithPasswordButton + text: qsTr("Connect") + enabled: ssidSelector.selectedSSID.length > 0 && passwordField.text.length > 0 + onClicked: { + if (ssidSelector.selectedSSID && passwordField.text) { + appendLog("Connecting to: " + ssidSelector.selectedSSID + " (with password)"); + // Find the network to get BSSID + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + const bssid = network ? network.bssid : ""; + Nmcli.connectWireless(ssidSelector.selectedSSID, passwordField.text, bssid, (result) => { + if (result.success) { + appendLog("Connection succeeded!"); + // Clear password field + passwordField.text = ""; + // Refresh network list after connection + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else { + appendLog("Connection failed: " + (result.error || "Unknown error")); + if (result.exitCode !== 0) { + appendLog("Exit code: " + result.exitCode); + } + // Refresh network list anyway to check status + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } + }); + appendLog("Connection initiated, tracking pending connection..."); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + const bssid = network && network.bssid ? network.bssid : "N/A"; + return qsTr("BSSID: %1").arg(bssid); + } + } + + Item { + Layout.fillWidth: true + } + } + } + } + + // Saved Connection Profiles Section + StyledRect { + Layout.fillWidth: true + implicitHeight: savedProfilesLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: savedProfilesLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Saved Connection Profiles") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Connections: %1").arg(Nmcli.savedConnections.length) + } + + StyledText { + text: qsTr("WiFi SSIDs: %1").arg(Nmcli.savedConnectionSsids.length) + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh") + onClicked: { + appendLog("Refreshing saved connections..."); + Nmcli.loadSavedConnections((ssids) => { + appendLog("Found " + Nmcli.savedConnections.length + " saved connections"); + appendLog("Found " + Nmcli.savedConnectionSsids.length + " WiFi SSIDs"); + }); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Selected SSID: %1").arg(ssidSelector.selectedSSID || "None") + } + + StyledText { + visible: ssidSelector.selectedSSID.length > 0 + text: { + if (!ssidSelector.selectedSSID) return ""; + const hasProfile = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); + return hasProfile ? qsTr("[Saved Profile]") : qsTr("[Not Saved]"); + } + color: { + if (!ssidSelector.selectedSSID) return Colours.palette.m3onSurface; + const hasProfile = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); + return hasProfile ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant; + } + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Check Profile") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const hasProfile = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); + appendLog("Profile check for '" + ssidSelector.selectedSSID + "': " + (hasProfile ? "Saved" : "Not saved")); + } + } + } + + TextButton { + text: qsTr("Forget Network") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + appendLog("Forgetting network: " + ssidSelector.selectedSSID); + Nmcli.forgetNetwork(ssidSelector.selectedSSID, (result) => { + if (result.success) { + appendLog("Network forgotten successfully"); + } else { + appendLog("Failed to forget network: " + (result.error || "Unknown error")); + } + }); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + TextButton { + text: qsTr("List All Saved SSIDs") + onClicked: { + appendLog("Saved WiFi SSIDs:"); + if (Nmcli.savedConnectionSsids.length === 0) { + appendLog(" No saved SSIDs"); + } else { + for (let i = 0; i < Nmcli.savedConnectionSsids.length; i++) { + appendLog(" " + (i + 1) + ". " + Nmcli.savedConnectionSsids[i]); + } + } + } + } + + TextButton { + text: qsTr("List All Connections") + onClicked: { + appendLog("Saved Connections:"); + if (Nmcli.savedConnections.length === 0) { + appendLog(" No saved connections"); + } else { + for (let i = 0; i < Nmcli.savedConnections.length; i++) { + appendLog(" " + (i + 1) + ". " + Nmcli.savedConnections[i]); + } + } + } + } + } + } + } + + // Pending Connection Tracking Section + StyledRect { + Layout.fillWidth: true + implicitHeight: pendingConnectionLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: pendingConnectionLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Pending Connection Tracking") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Status: %1").arg(Nmcli.pendingConnection ? "Connecting..." : "No pending connection") + color: Nmcli.pendingConnection ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + StyledText { + visible: Nmcli.pendingConnection + text: qsTr("SSID: %1").arg(Nmcli.pendingConnection ? Nmcli.pendingConnection.ssid : "") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Check Status") + onClicked: { + if (Nmcli.pendingConnection) { + appendLog("Pending connection: " + Nmcli.pendingConnection.ssid); + appendLog("BSSID: " + (Nmcli.pendingConnection.bssid || "N/A")); + const connected = Nmcli.active && Nmcli.active.ssid === Nmcli.pendingConnection.ssid; + appendLog("Connected: " + (connected ? "Yes" : "No")); + if (connected) { + appendLog("Connection succeeded!"); + } else { + appendLog("Still connecting..."); + } + } else { + appendLog("No pending connection"); + } + } + } + + TextButton { + text: qsTr("Clear Pending") + enabled: Nmcli.pendingConnection !== null + onClicked: { + if (Nmcli.pendingConnection) { + appendLog("Clearing pending connection: " + Nmcli.pendingConnection.ssid); + Nmcli.pendingConnection = null; + appendLog("Pending connection cleared"); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Active Network: %1").arg(Nmcli.active ? Nmcli.active.ssid : "None") + color: Nmcli.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh Networks & Check") + onClicked: { + appendLog("Refreshing network list to check pending connection..."); + Nmcli.getNetworks((networks) => { + appendLog("Network list refreshed"); + if (Nmcli.pendingConnection) { + const connected = Nmcli.active && Nmcli.active.ssid === Nmcli.pendingConnection.ssid; + appendLog("Pending connection check: " + (connected ? "Connected!" : "Still connecting...")); + } + }); + } + } + } + } + } + + // Connection Failure Handling Section + StyledRect { + Layout.fillWidth: true + implicitHeight: connectionFailureLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: connectionFailureLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Connection Failure Handling") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Last Failed SSID: %1").arg(lastFailedSsid || "None") + color: lastFailedSsid ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Clear Failure") + enabled: lastFailedSsid.length > 0 + onClicked: { + lastFailedSsid = ""; + appendLog("Cleared failure status"); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Test Password Detection") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Secure Network (No Password)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (network && network.isSecure) { + appendLog("Testing connection to secure network without password (should detect password requirement)"); + const bssid = network ? network.bssid : ""; + Nmcli.connectWireless(ssidSelector.selectedSSID, "", bssid, (result) => { + if (result.needsPassword) { + appendLog("✓ Password requirement detected correctly!"); + appendLog("Error: " + (result.error || "N/A")); + } else if (result.success) { + appendLog("Connection succeeded (saved password used)"); + } else { + appendLog("Connection failed: " + (result.error || "Unknown error")); + } + }); + } else { + appendLog("Selected network is not secure, cannot test password detection"); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Connection Retry Test") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Retry Logic") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + appendLog("Testing connection retry logic (will retry up to 2 times on failure)"); + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + const bssid = network ? network.bssid : ""; + // Use invalid password to trigger failure + Nmcli.connectWireless(ssidSelector.selectedSSID, "invalid_password_test", bssid, (result) => { + if (result.success) { + appendLog("Connection succeeded (unexpected)"); + } else { + appendLog("Connection failed after retries: " + (result.error || "Unknown error")); + } + }); + } + } + } + } + } + } + + // Password Callback Handling Section + StyledRect { + Layout.fillWidth: true + implicitHeight: passwordCallbackLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: passwordCallbackLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Password Callback Handling") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Selected SSID: %1").arg(ssidSelector.selectedSSID || "None") + } + + StyledText { + visible: ssidSelector.selectedSSID.length > 0 + text: { + if (!ssidSelector.selectedSSID) return ""; + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (!network) return ""; + return network.isSecure ? qsTr("[Secure]") : qsTr("[Open]"); + } + color: { + if (!ssidSelector.selectedSSID) return Colours.palette.m3onSurface; + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (!network) return Colours.palette.m3onSurface; + return network.isSecure ? Colours.palette.m3error : Colours.palette.m3primary; + } + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Password Check (Secure)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (network && network.isSecure) { + appendLog("Testing password check for secure network: " + ssidSelector.selectedSSID); + appendLog("This will try saved password first, then prompt if needed"); + const bssid = network ? network.bssid : ""; + Nmcli.connectToNetworkWithPasswordCheck(ssidSelector.selectedSSID, true, (result) => { + if (result.success) { + if (result.usedSavedPassword) { + appendLog("✓ Connection succeeded using saved password!"); + } else { + appendLog("✓ Connection succeeded!"); + } + // Refresh network list + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else if (result.needsPassword) { + appendLog("→ Password required - callback triggered"); + appendLog(" Error: " + (result.error || "N/A")); + appendLog(" (In real UI, this would show password dialog)"); + } else { + appendLog("✗ Connection failed: " + (result.error || "Unknown error")); + } + }, bssid); + } else { + appendLog("Selected network is not secure, cannot test password check"); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Test Open Network") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Password Check (Open)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (network && !network.isSecure) { + appendLog("Testing password check for open network: " + ssidSelector.selectedSSID); + appendLog("Open networks should connect directly without password"); + const bssid = network ? network.bssid : ""; + Nmcli.connectToNetworkWithPasswordCheck(ssidSelector.selectedSSID, false, (result) => { + if (result.success) { + appendLog("✓ Connection succeeded!"); + // Refresh network list + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else { + appendLog("✗ Connection failed: " + (result.error || "Unknown error")); + } + }, bssid); + } else { + appendLog("Selected network is not open, cannot test open network handling"); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Test with Saved Password") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test Secure Network (Has Saved Password)") + enabled: ssidSelector.selectedSSID.length > 0 + onClicked: { + if (ssidSelector.selectedSSID) { + const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); + if (network && network.isSecure) { + const hasSaved = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); + appendLog("Testing password check for: " + ssidSelector.selectedSSID); + appendLog("Has saved profile: " + (hasSaved ? "Yes" : "No")); + if (hasSaved) { + appendLog("This should connect using saved password without prompting"); + } else { + appendLog("This should prompt for password since no saved profile exists"); + } + const bssid = network ? network.bssid : ""; + Nmcli.connectToNetworkWithPasswordCheck(ssidSelector.selectedSSID, true, (result) => { + if (result.success) { + if (result.usedSavedPassword) { + appendLog("✓ Connection succeeded using saved password!"); + } else { + appendLog("✓ Connection succeeded!"); + } + // Refresh network list + Qt.callLater(() => { + Nmcli.getNetworks(() => {}); + }, 1000); + } else if (result.needsPassword) { + appendLog("→ Password required - callback triggered"); + appendLog(" (In real UI, this would show password dialog)"); + } else { + appendLog("✗ Connection failed: " + (result.error || "Unknown error")); + } + }, bssid); + } else { + appendLog("Selected network is not secure, cannot test saved password"); + } + } + } + } + } + } + } + + // Device Details Parsing Section + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceDetailsLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: deviceDetailsLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Device Details Parsing") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Wireless Device Details") + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Get Wireless Details") + onClicked: { + const activeInterface = interfaceSelector.selectedInterface; + if (activeInterface && activeInterface.length > 0) { + appendLog("Getting wireless device details for: " + activeInterface); + Nmcli.getWirelessDeviceDetails(activeInterface, (details) => { + if (details) { + appendLog("Wireless Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("Failed to get wireless device details"); + } + }); + } else { + appendLog("Getting wireless device details for active interface"); + Nmcli.getWirelessDeviceDetails("", (details) => { + if (details) { + appendLog("Wireless Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("No active wireless interface or failed to get details"); + } + }); + } + } + } + + TextButton { + text: qsTr("Show Current") + onClicked: { + if (Nmcli.wirelessDeviceDetails) { + const details = Nmcli.wirelessDeviceDetails; + appendLog("Current Wireless Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("No wireless device details available"); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Ethernet Device Details") + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Get Ethernet Details") + onClicked: { + const activeInterface = interfaceSelector.selectedInterface; + if (activeInterface && activeInterface.length > 0) { + appendLog("Getting ethernet device details for: " + activeInterface); + Nmcli.getEthernetDeviceDetails(activeInterface, (details) => { + if (details) { + appendLog("Ethernet Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" Speed: " + (details.speed || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("Failed to get ethernet device details"); + } + }); + } else { + appendLog("Getting ethernet device details for active interface"); + Nmcli.getEthernetDeviceDetails("", (details) => { + if (details) { + appendLog("Ethernet Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" Speed: " + (details.speed || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("No active ethernet interface or failed to get details"); + } + }); + } + } + } + + TextButton { + text: qsTr("Show Current") + onClicked: { + if (Nmcli.ethernetDeviceDetails) { + const details = Nmcli.ethernetDeviceDetails; + appendLog("Current Ethernet Device Details:"); + appendLog(" IP Address: " + (details.ipAddress || "N/A")); + appendLog(" Gateway: " + (details.gateway || "N/A")); + appendLog(" Subnet: " + (details.subnet || "N/A")); + appendLog(" MAC Address: " + (details.macAddress || "N/A")); + appendLog(" Speed: " + (details.speed || "N/A")); + appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); + } else { + appendLog("No ethernet device details available"); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - StyledText { - text: qsTr("Debug Panel") - font.pointSize: Appearance.font.size.larger - font.weight: 500 + StyledText { + text: qsTr("CIDR to Subnet Mask Test") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Test CIDR Conversion") + onClicked: { + appendLog("Testing CIDR to Subnet Mask conversion:"); + const testCases = ["8", "16", "24", "32", "0", "25", "30"]; + for (let i = 0; i < testCases.length; i++) { + const cidr = testCases[i]; + const subnet = Nmcli.cidrToSubnetMask(cidr); + appendLog(" /" + cidr + " -> " + (subnet || "Invalid")); + } + } + } + } + } } - // Action Buttons Section + // Connection Status Monitoring Section StyledRect { Layout.fillWidth: true - implicitHeight: buttonsLayout.implicitHeight + Appearance.padding.large * 2 + implicitHeight: connectionMonitoringLayout.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { - id: buttonsLayout + id: connectionMonitoringLayout anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { - text: qsTr("Actions") + text: qsTr("Connection Status Monitoring") font.pointSize: Appearance.font.size.normal font.weight: 500 } - Flow { + RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.small + StyledText { + text: qsTr("Active Network: %1").arg(Nmcli.active ? Nmcli.active.ssid : "None") + color: Nmcli.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + StyledText { + visible: Nmcli.active + text: Nmcli.active ? qsTr("Signal: %1%").arg(Nmcli.active.strength) : "" + } + + Item { + Layout.fillWidth: true + } + TextButton { - text: qsTr("Clear Log") + text: qsTr("Refresh Networks") onClicked: { - debugOutput.text = ""; - appendLog("Debug log cleared"); + appendLog("Manually refreshing network list..."); + Nmcli.getNetworks((networks) => { + appendLog("Network list refreshed: " + networks.length + " networks"); + if (Nmcli.active) { + appendLog("Active network: " + Nmcli.active.ssid); + } else { + appendLog("No active network"); + } + }); } } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Monitor Status") + } + + StyledText { + text: qsTr("Monitoring connection changes (automatic refresh enabled)") + color: Colours.palette.m3primary + } + + Item { + Layout.fillWidth: true + } TextButton { - text: qsTr("Test Action") + text: qsTr("Test Connection Change") onClicked: { - appendLog("Test action executed at " + new Date().toLocaleTimeString()); + appendLog("Testing connection change detection..."); + appendLog("This will trigger a manual refresh to simulate a connection change"); + Nmcli.refreshOnConnectionChange(); + appendLog("Refresh triggered - check if network list and device details updated"); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Device Details Auto-Refresh") + } + + StyledText { + text: { + if (Nmcli.wirelessDeviceDetails) { + return qsTr("Wireless: %1").arg(Nmcli.wirelessDeviceDetails.ipAddress || "N/A"); + } else if (Nmcli.ethernetDeviceDetails) { + return qsTr("Ethernet: %1").arg(Nmcli.ethernetDeviceDetails.ipAddress || "N/A"); + } else { + return qsTr("No device details"); + } } + color: (Nmcli.wirelessDeviceDetails || Nmcli.ethernetDeviceDetails) ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true } TextButton { - text: qsTr("Log Network State") + text: qsTr("Refresh Device Details") onClicked: { - appendLog("Network state:"); - appendLog(" Active: " + (root.session.network.active ? "Yes" : "No")); + appendLog("Manually refreshing device details..."); + if (Nmcli.active && Nmcli.active.active) { + appendLog("Active network detected, refreshing device details..."); + // Refresh wireless device details + if (Nmcli.wirelessInterfaces.length > 0) { + const activeWireless = Nmcli.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeWireless && activeWireless.device) { + Nmcli.getWirelessDeviceDetails(activeWireless.device, (details) => { + if (details) { + appendLog("Wireless device details refreshed"); + } + }); + } + } + // Refresh ethernet device details + if (Nmcli.ethernetInterfaces.length > 0) { + const activeEthernet = Nmcli.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeEthernet && activeEthernet.device) { + Nmcli.getEthernetDeviceDetails(activeEthernet.device, (details) => { + if (details) { + appendLog("Ethernet device details refreshed"); + } + }); + } + } + } else { + appendLog("No active network, clearing device details"); + Nmcli.wirelessDeviceDetails = null; + Nmcli.ethernetDeviceDetails = null; + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Connection Events") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Show Active Network Info") + onClicked: { + if (Nmcli.active) { + appendLog("Active Network Information:"); + appendLog(" SSID: " + Nmcli.active.ssid); + appendLog(" BSSID: " + (Nmcli.active.bssid || "N/A")); + appendLog(" Signal: " + Nmcli.active.strength + "%"); + appendLog(" Frequency: " + Nmcli.active.frequency + " MHz"); + appendLog(" Security: " + (Nmcli.active.security || "Open")); + appendLog(" Is Secure: " + (Nmcli.active.isSecure ? "Yes" : "No")); + } else { + appendLog("No active network"); + } + } + } + } + } + } + + // Ethernet Device Management Section + StyledRect { + Layout.fillWidth: true + implicitHeight: ethernetManagementLayout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: ethernetManagementLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Ethernet Device Management") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Ethernet Devices: %1").arg(Nmcli.ethernetDevices.length) + } + + StyledText { + text: qsTr("Active: %1").arg(Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : "None") + color: Nmcli.activeEthernet ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Refresh Devices") + onClicked: { + appendLog("Refreshing ethernet devices..."); + Nmcli.getEthernetInterfaces((interfaces) => { + appendLog("Found " + Nmcli.ethernetDevices.length + " ethernet devices"); + for (let i = 0; i < Nmcli.ethernetDevices.length; i++) { + const dev = Nmcli.ethernetDevices[i]; + appendLog(" " + (i + 1) + ". " + dev.interface + " - " + dev.state + (dev.connected ? " [Connected]" : "")); + } + if (Nmcli.activeEthernet) { + appendLog("Active ethernet: " + Nmcli.activeEthernet.interface); + } + }); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Selected Interface: %1").arg(interfaceSelector.selectedInterface || "None") + } + + StyledText { + visible: interfaceSelector.selectedInterface.length > 0 + text: { + if (!interfaceSelector.selectedInterface) return ""; + const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); + if (!device) return ""; + return device.connected ? qsTr("[Connected]") : qsTr("[Disconnected]"); + } + color: { + if (!interfaceSelector.selectedInterface) return Colours.palette.m3onSurface; + const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); + if (!device) return Colours.palette.m3onSurface; + return device.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant; + } + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("Connect Ethernet") + enabled: interfaceSelector.selectedInterface.length > 0 + onClicked: { + if (interfaceSelector.selectedInterface) { + const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); + if (device) { + appendLog("Connecting ethernet: " + interfaceSelector.selectedInterface); + appendLog("Connection name: " + (device.connection || "N/A")); + Nmcli.connectEthernet(device.connection || "", interfaceSelector.selectedInterface, (result) => { + if (result.success) { + appendLog("✓ Ethernet connection initiated"); + appendLog("Refreshing device list..."); + } else { + appendLog("✗ Failed to connect: " + (result.error || "Unknown error")); + } + }); + } else { + appendLog("Device not found in ethernet devices list"); + } + } + } + } + + TextButton { + text: qsTr("Disconnect Ethernet") + enabled: interfaceSelector.selectedInterface.length > 0 + onClicked: { + if (interfaceSelector.selectedInterface) { + const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); + if (device && device.connection) { + appendLog("Disconnecting ethernet: " + device.connection); + Nmcli.disconnectEthernet(device.connection, (result) => { + if (result.success) { + appendLog("✓ Ethernet disconnected"); + appendLog("Refreshing device list..."); + } else { + appendLog("✗ Failed to disconnect: " + (result.error || "Unknown error")); + } + }); + } else { + appendLog("No connection name available for this device"); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("List All Ethernet Devices") + } + + Item { + Layout.fillWidth: true + } + + TextButton { + text: qsTr("List Devices") + onClicked: { + appendLog("Ethernet Devices:"); + if (Nmcli.ethernetDevices.length === 0) { + appendLog(" No ethernet devices found"); + } else { + for (let i = 0; i < Nmcli.ethernetDevices.length; i++) { + const dev = Nmcli.ethernetDevices[i]; + appendLog(" " + (i + 1) + ". " + dev.interface); + appendLog(" Type: " + dev.type); + appendLog(" State: " + dev.state); + appendLog(" Connection: " + (dev.connection || "None")); + appendLog(" Connected: " + (dev.connected ? "Yes" : "No")); + } + } + } + } + + TextButton { + text: qsTr("Show Active Device") + onClicked: { + if (Nmcli.activeEthernet) { + appendLog("Active Ethernet Device:"); + appendLog(" Interface: " + Nmcli.activeEthernet.interface); + appendLog(" State: " + Nmcli.activeEthernet.state); + appendLog(" Connection: " + (Nmcli.activeEthernet.connection || "None")); + } else { + appendLog("No active ethernet device"); + } } } } @@ -82,7 +1877,8 @@ Item { // Debug Output Section StyledRect { Layout.fillWidth: true - Layout.fillHeight: true + Layout.preferredHeight: 300 + Layout.minimumHeight: 200 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer @@ -116,17 +1912,17 @@ Item { } StyledFlickable { - id: flickable + id: debugOutputFlickable Layout.fillWidth: true Layout.fillHeight: true flickableDirection: Flickable.VerticalFlick - contentHeight: debugOutput.implicitHeight + clip: true TextEdit { id: debugOutput - width: flickable.width + width: debugOutputFlickable.width readOnly: true wrapMode: TextEdit.Wrap font.family: Appearance.font.family.mono @@ -143,19 +1939,25 @@ Item { onTextChanged: { // Ensure color stays set when text changes color = Colours.palette.m3primary; - if (flickable.contentHeight > flickable.height) { - flickable.contentY = flickable.contentHeight - flickable.height; - } + // Update content height + debugOutputFlickable.contentHeight = Math.max(implicitHeight, debugOutputFlickable.height); + // Auto-scroll to bottom + Qt.callLater(() => { + if (debugOutputFlickable.contentHeight > debugOutputFlickable.height) { + debugOutputFlickable.contentY = debugOutputFlickable.contentHeight - debugOutputFlickable.height; + } + }); } } } StyledScrollBar { - flickable: flickable + flickable: debugOutputFlickable policy: ScrollBar.AlwaysOn } } } + } } function appendLog(message: string): void { @@ -166,5 +1968,12 @@ Item { function log(message: string): void { appendLog(message); } + + Component.onCompleted: { + // Set up debug logger for Nmcli service + Nmcli.setDebugLogger((msg) => { + appendLog("[Nmcli] " + msg); + }); + } } diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..4762ef0 --- /dev/null +++ b/plan.md @@ -0,0 +1,137 @@ +# Nmcli.qml Feature Completion Plan + +This document outlines the missing features needed in `Nmcli.qml` to replace `Network.qml` or rewrite the wireless panel in the control center. + +## Current Status + +`Nmcli.qml` currently has: +- ✅ Device status queries +- ✅ Wireless/Ethernet interface listing +- ✅ Interface connection status checking +- ✅ Basic wireless connection (SSID + password) +- ✅ Disconnect functionality +- ✅ Device details (basic) +- ✅ Interface up/down +- ✅ WiFi scanning +- ✅ SSID listing with signal/security (sorted) + +## Missing Features + +### 1. WiFi Radio Control +- [x] `enableWifi(enabled: bool)` - Turn WiFi radio on/off +- [x] `toggleWifi()` - Toggle WiFi radio state +- [x] `wifiEnabled` property - Current WiFi radio state +- [x] Monitor WiFi radio state changes + +**Implementation Notes:** +- Use `nmcli radio wifi on/off` +- Monitor state with `nmcli radio wifi` +- Update `wifiEnabled` property on state changes + +### 2. Network List Management +- [x] `networks` property - List of AccessPoint objects +- [x] `active` property - Currently active network +- [x] Real-time network list updates +- [x] Network grouping by SSID with signal prioritization +- [x] AccessPoint component/object with properties: + - `ssid`, `bssid`, `strength`, `frequency`, `active`, `security`, `isSecure` + +**Implementation Notes:** +- Use `nmcli -g ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY d w` +- Parse and group networks by SSID +- Prioritize active/connected networks +- Update network list on connection changes + +### 3. Connection Management - BSSID Support +- [x] BSSID support in `connectWireless()` function +- [x] Connection profile creation with BSSID (`createConnectionWithPassword`) +- [x] Handle BSSID in connection commands + +**Implementation Notes:** +- Use `nmcli connection add` with `802-11-wireless.bssid` for BSSID-based connections +- Fallback to SSID-only connection if BSSID not available +- Handle existing connection profiles when BSSID is provided + +### 4. Saved Connection Profile Management +- [x] `savedConnections` property - List of saved connection names +- [x] `savedConnectionSsids` property - List of saved SSIDs +- [x] `hasSavedProfile(ssid: string)` function - Check if profile exists +- [x] `forgetNetwork(ssid: string)` function - Delete connection profile +- [x] Load saved connections on startup +- [x] Update saved connections list after connection changes + +**Implementation Notes:** +- Use `nmcli -t -f NAME,TYPE connection show` to list connections +- Query SSIDs for WiFi connections: `nmcli -t -f 802-11-wireless.ssid connection show ` +- Use `nmcli connection delete ` to forget networks +- Case-insensitive SSID matching + +### 5. Pending Connection Tracking +- [x] `pendingConnection` property - Track connection in progress +- [x] Connection state tracking with timers +- [x] Connection success/failure detection +- [x] Automatic retry or callback on failure + +**Implementation Notes:** +- Track pending connection with SSID/BSSID +- Use timers to check connection status +- Monitor network list updates to detect successful connection +- Handle connection failures and trigger callbacks + +### 6. Connection Failure Handling +- [x] `connectionFailed(ssid: string)` signal +- [x] Password requirement detection from error messages +- [x] Connection retry logic +- [x] Error message parsing and reporting + +**Implementation Notes:** +- Parse stderr output for password requirements +- Detect specific error patterns (e.g., "Secrets were required") +- Emit signals for UI to handle password dialogs +- Provide meaningful error messages + +### 7. Password Callback Handling +- [x] `connectToNetworkWithPasswordCheck()` function +- [x] Try connection without password first (use saved password) +- [x] Callback on password requirement +- [x] Handle both secure and open networks + +**Implementation Notes:** +- Attempt connection without password for secure networks +- If connection fails with password error, trigger callback +- For open networks, connect directly +- Support callback pattern for password dialogs + +### 8. Device Details Parsing +- [x] Full parsing of `device show` output +- [x] `wirelessDeviceDetails` property with: + - `ipAddress`, `gateway`, `dns[]`, `subnet`, `macAddress` +- [x] `ethernetDeviceDetails` property with: + - `ipAddress`, `gateway`, `dns[]`, `subnet`, `macAddress`, `speed` +- [x] `cidrToSubnetMask()` helper function +- [x] Update device details on connection changes + +**Implementation Notes:** +- Parse `nmcli device show ` output +- Extract IP4.ADDRESS, IP4.GATEWAY, IP4.DNS, etc. +- Convert CIDR notation to subnet mask +- Handle both wireless and ethernet device details + +### 9. Connection Status Monitoring +- [x] Automatic network list refresh on connection changes +- [x] Monitor connection state changes +- [x] Update active network property +- [x] Refresh device details on connection + +**Implementation Notes:** +- Use Process stdout SplitParser to monitor changes +- Trigger network list refresh on connection events +- Update `active` property when connection changes +- Refresh device details when connected + +### 10. Ethernet Device Management +- [x] `ethernetDevices` property - List of ethernet devices +- [x] `activeEthernet` property - Currently active ethernet device +- [x] `connectEthernet(connectionName, interfaceName)` function +- [x] `disconnectEthernet(connectionName)` function +- [x] Ethernet device details parsing diff --git a/services/Nmcli.qml b/services/Nmcli.qml new file mode 100644 index 0000000..4e45b41 --- /dev/null +++ b/services/Nmcli.qml @@ -0,0 +1,1246 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + property var deviceStatus: null + property var wirelessInterfaces: [] + property var ethernetInterfaces: [] + property bool isConnected: false + property string activeInterface: "" + property string activeConnection: "" + property bool wifiEnabled: true + readonly property list networks: [] + readonly property AccessPoint active: networks.find(n => n.active) ?? null + property list savedConnections: [] + property list savedConnectionSsids: [] + + property var wifiConnectionQueue: [] + property int currentSsidQueryIndex: 0 + property var pendingConnection: null + signal connectionFailed(string ssid) + property var wirelessDeviceDetails: null + property var ethernetDeviceDetails: null + property list ethernetDevices: [] + readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null + + property list activeProcesses: [] + property var debugLogger: null + + function setDebugLogger(logger: var): void { + root.debugLogger = logger; + } + + function log(message: string): void { + if (root.debugLogger) { + root.debugLogger(message); + } else { + console.log("[Nmcli]", message); + } + } + + function appendLog(message: string): void { + log(message); + } + + function executeCommand(args: list, callback: var): void { + const proc = commandProc.createObject(root); + proc.command = ["nmcli", ...args]; + proc.callback = callback; + + activeProcesses.push(proc); + + proc.processFinished.connect(() => { + const index = activeProcesses.indexOf(proc); + if (index >= 0) { + activeProcesses.splice(index, 1); + } + }); + + Qt.callLater(() => { + proc.exec(proc.command); + }); + } + + function getDeviceStatus(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + if (callback) callback(result.output); + }); + } + + function getWirelessInterfaces(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + const interfaces = []; + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2 && parts[1] === "wifi") { + interfaces.push({ + device: parts[0] || "", + type: parts[1] || "", + state: parts[2] || "", + connection: parts[3] || "" + }); + } + } + root.wirelessInterfaces = interfaces; + if (callback) callback(interfaces); + }); + } + + function getEthernetInterfaces(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + const interfaces = []; + const devices = []; + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2 && parts[1] === "ethernet") { + const device = parts[0] || ""; + const type = parts[1] || ""; + const state = parts[2] || ""; + const connection = parts[3] || ""; + + const connected = state === "100 (connected)" || state === "connected" || state.startsWith("connected"); + + interfaces.push({ + device: device, + type: type, + state: state, + connection: connection + }); + + devices.push({ + interface: device, + type: type, + state: state, + connection: connection, + connected: connected, + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }); + } + } + root.ethernetInterfaces = interfaces; + root.ethernetDevices = devices; + if (callback) callback(interfaces); + }); + } + + function connectEthernet(connectionName: string, interfaceName: string, callback: var): void { + if (connectionName && connectionName.length > 0) { + executeCommand(["connection", "up", connectionName], (result) => { + if (result.success) { + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + if (interfaceName && interfaceName.length > 0) { + Qt.callLater(() => { + getEthernetDeviceDetails(interfaceName, () => {}); + }, 1000); + } + }, 500); + } + if (callback) callback(result); + }); + } else if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "connect", interfaceName], (result) => { + if (result.success) { + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + Qt.callLater(() => { + getEthernetDeviceDetails(interfaceName, () => {}); + }, 1000); + }, 500); + } + if (callback) callback(result); + }); + } else { + if (callback) callback({ success: false, output: "", error: "No connection name or interface specified", exitCode: -1 }); + } + } + + function disconnectEthernet(connectionName: string, callback: var): void { + if (!connectionName || connectionName.length === 0) { + if (callback) callback({ success: false, output: "", error: "No connection name specified", exitCode: -1 }); + return; + } + + executeCommand(["connection", "down", connectionName], (result) => { + if (result.success) { + root.ethernetDeviceDetails = null; + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + }, 500); + } + if (callback) callback(result); + }); + } + + function getAllInterfaces(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + const interfaces = []; + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2 && (parts[1] === "wifi" || parts[1] === "ethernet")) { + interfaces.push({ + device: parts[0] || "", + type: parts[1] || "", + state: parts[2] || "", + connection: parts[3] || "" + }); + } + } + if (callback) callback(interfaces); + }); + } + + function isInterfaceConnected(interfaceName: string, callback: var): void { + executeCommand(["device", "status"], (result) => { + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(/\s+/); + if (parts.length >= 3 && parts[0] === interfaceName) { + const connected = parts[2] === "connected" || parts[2].startsWith("connected"); + if (callback) callback(connected); + return; + } + } + if (callback) callback(false); + }); + } + + function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { + if (isSecure) { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + connectWireless(ssid, "", bssid, (result) => { + if (result.success) { + if (callback) callback({ success: true, usedSavedPassword: true, output: result.output, error: "", exitCode: 0 }); + } else if (result.needsPassword) { + if (callback) callback({ success: false, needsPassword: true, output: result.output, error: result.error, exitCode: result.exitCode }); + } else { + if (callback) callback(result); + } + }); + } else { + connectWireless(ssid, "", bssid, callback); + } + } + + function connectWireless(ssid: string, password: string, bssid: string, callback: var, retryCount: int): void { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + const retries = retryCount !== undefined ? retryCount : 0; + const maxRetries = 2; + + if (callback) { + root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback, retryCount: retries }; + connectionCheckTimer.start(); + immediateCheckTimer.checkCount = 0; + immediateCheckTimer.start(); + } + + if (password && password.length > 0 && hasBssid) { + const bssidUpper = bssid.toUpperCase(); + createConnectionWithPassword(ssid, bssidUpper, password, callback); + return; + } + + let cmd = ["device", "wifi", "connect", ssid]; + if (password && password.length > 0) { + cmd.push("password", password); + } + executeCommand(cmd, (result) => { + if (result.needsPassword && callback) { + if (callback) callback(result); + return; + } + + if (!result.success && root.pendingConnection && retries < maxRetries) { + log("Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); + Qt.callLater(() => { + connectWireless(ssid, password, bssid, callback, retries + 1); + }, 1000); + } else if (!result.success && root.pendingConnection) { + } else if (result.success && callback) { + } else if (!result.success && !root.pendingConnection) { + if (callback) callback(result); + } + }); + } + + function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void { + checkAndDeleteConnection(ssid, () => { + const cmd = ["connection", "add", + "type", "wifi", + "con-name", ssid, + "ifname", "*", + "ssid", ssid, + "802-11-wireless.bssid", bssidUpper, + "802-11-wireless-security.key-mgmt", "wpa-psk", + "802-11-wireless-security.psk", password]; + + executeCommand(cmd, (result) => { + if (result.success) { + loadSavedConnections(() => {}); + activateConnection(ssid, callback); + } else { + const hasDuplicateWarning = result.error && ( + result.error.includes("another connection with the name") || + result.error.includes("Reference the connection by its uuid") + ); + + if (hasDuplicateWarning || (result.exitCode > 0 && result.exitCode < 10)) { + loadSavedConnections(() => {}); + activateConnection(ssid, callback); + } else { + log("Connection profile creation failed, trying fallback..."); + let fallbackCmd = ["device", "wifi", "connect", ssid, "password", password]; + executeCommand(fallbackCmd, (fallbackResult) => { + if (callback) callback(fallbackResult); + }); + } + } + }); + }); + } + + function checkAndDeleteConnection(ssid: string, callback: var): void { + executeCommand(["connection", "show", ssid], (result) => { + if (result.success) { + executeCommand(["connection", "delete", ssid], (deleteResult) => { + Qt.callLater(() => { + if (callback) callback(); + }, 300); + }); + } else { + if (callback) callback(); + } + }); + } + + function activateConnection(connectionName: string, callback: var): void { + executeCommand(["connection", "up", connectionName], (result) => { + if (callback) callback(result); + }); + } + + function loadSavedConnections(callback: var): void { + executeCommand(["-t", "-f", "NAME,TYPE", "connection", "show"], (result) => { + if (!result.success) { + root.savedConnections = []; + root.savedConnectionSsids = []; + if (callback) callback([]); + return; + } + + parseConnectionList(result.output, callback); + }); + } + + function parseConnectionList(output: string, callback: var): void { + const lines = output.trim().split("\n").filter(line => line.length > 0); + const wifiConnections = []; + const connections = []; + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2) { + const name = parts[0]; + const type = parts[1]; + connections.push(name); + + if (type === "802-11-wireless") { + wifiConnections.push(name); + } + } + } + + root.savedConnections = connections; + + if (wifiConnections.length > 0) { + root.wifiConnectionQueue = wifiConnections; + root.currentSsidQueryIndex = 0; + root.savedConnectionSsids = []; + queryNextSsid(callback); + } else { + root.savedConnectionSsids = []; + root.wifiConnectionQueue = []; + if (callback) callback(root.savedConnectionSsids); + } + } + + function queryNextSsid(callback: var): void { + if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { + const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; + root.currentSsidQueryIndex++; + + executeCommand(["-t", "-f", "802-11-wireless.ssid", "connection", "show", connectionName], (result) => { + if (result.success) { + processSsidOutput(result.output); + } + queryNextSsid(callback); + }); + } else { + root.wifiConnectionQueue = []; + root.currentSsidQueryIndex = 0; + if (callback) callback(root.savedConnectionSsids); + } + } + + function processSsidOutput(output: string): void { + const lines = output.trim().split("\n"); + for (const line of lines) { + if (line.startsWith("802-11-wireless.ssid:")) { + const ssid = line.substring("802-11-wireless.ssid:".length).trim(); + if (ssid && ssid.length > 0) { + const ssidLower = ssid.toLowerCase(); + const exists = root.savedConnectionSsids.some(s => s && s.toLowerCase() === ssidLower); + if (!exists) { + const newList = root.savedConnectionSsids.slice(); + newList.push(ssid); + root.savedConnectionSsids = newList; + } + } + } + } + } + + function hasSavedProfile(ssid: string): bool { + if (!ssid || ssid.length === 0) { + return false; + } + const ssidLower = ssid.toLowerCase().trim(); + + if (root.active && root.active.ssid) { + const activeSsidLower = root.active.ssid.toLowerCase().trim(); + if (activeSsidLower === ssidLower) { + return true; + } + } + + const hasSsid = root.savedConnectionSsids.some(savedSsid => + savedSsid && savedSsid.toLowerCase().trim() === ssidLower + ); + + if (hasSsid) { + return true; + } + + const hasConnectionName = root.savedConnections.some(connName => + connName && connName.toLowerCase().trim() === ssidLower + ); + + return hasConnectionName; + } + + function forgetNetwork(ssid: string, callback: var): void { + if (!ssid || ssid.length === 0) { + if (callback) callback({ success: false, output: "", error: "No SSID specified", exitCode: -1 }); + return; + } + + const connectionName = root.savedConnections.find(conn => + conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() + ) || ssid; + + executeCommand(["connection", "delete", connectionName], (result) => { + if (result.success) { + Qt.callLater(() => { + loadSavedConnections(() => {}); + }, 500); + } + if (callback) callback(result); + }); + } + + function disconnect(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "disconnect", interfaceName], (result) => { + if (callback) callback(result.success ? result.output : ""); + }); + } else { + executeCommand(["device", "disconnect", "wifi"], (result) => { + if (callback) callback(result.success ? result.output : ""); + }); + } + } + + function getDeviceDetails(interfaceName: string, callback: var): void { + executeCommand(["device", "show", interfaceName], (result) => { + if (callback) callback(result.output); + }); + } + + function refreshStatus(callback: var): void { + getDeviceStatus((output) => { + const lines = output.trim().split("\n"); + let connected = false; + let activeIf = ""; + let activeConn = ""; + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 4) { + const state = parts[2] || ""; + if (state === "connected" || state.startsWith("connected")) { + connected = true; + activeIf = parts[0] || ""; + activeConn = parts[3] || ""; + break; + } + } + } + + root.isConnected = connected; + root.activeInterface = activeIf; + root.activeConnection = activeConn; + + if (callback) callback({ connected, interface: activeIf, connection: activeConn }); + }); + } + + function bringInterfaceUp(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "connect", interfaceName], (result) => { + if (callback) { + callback(result); + } + }); + } else { + if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + } + } + + function bringInterfaceDown(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "disconnect", interfaceName], (result) => { + if (callback) { + callback(result); + } + }); + } else { + if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + } + } + + function scanWirelessNetworks(interfaceName: string, callback: var): void { + let cmd = ["device", "wifi", "rescan"]; + if (interfaceName && interfaceName.length > 0) { + cmd.push("ifname", interfaceName); + } + executeCommand(cmd, (result) => { + if (callback) { + callback(result); + } + }); + } + + function enableWifi(enabled: bool, callback: var): void { + const cmd = enabled ? "on" : "off"; + executeCommand(["radio", "wifi", cmd], (result) => { + if (result.success) { + getWifiStatus((status) => { + root.wifiEnabled = status; + if (callback) callback(result); + }); + } else { + if (callback) callback(result); + } + }); + } + + function toggleWifi(callback: var): void { + const newState = !root.wifiEnabled; + enableWifi(newState, callback); + } + + function getWifiStatus(callback: var): void { + executeCommand(["radio", "wifi"], (result) => { + if (result.success) { + const enabled = result.output.trim() === "enabled"; + root.wifiEnabled = enabled; + if (callback) callback(enabled); + } else { + if (callback) callback(root.wifiEnabled); + } + }); + } + + function getNetworks(callback: var): void { + executeCommand(["-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"], (result) => { + if (!result.success) { + if (callback) callback([]); + return; + } + + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; + const rep = new RegExp("\\\\:", "g"); + const rep2 = new RegExp(PLACEHOLDER, "g"); + + const allNetworks = result.output.trim().split("\n") + .filter(line => line && line.length > 0) + .map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1] || "0", 10) || 0, + frequency: parseInt(net[2] || "0", 10) || 0, + ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), + bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), + security: (net[5] ?? "").trim() + }; + }) + .filter(n => n.ssid && n.ssid.length > 0); + + const networkMap = new Map(); + for (const network of allNetworks) { + const existing = networkMap.get(network.ssid); + if (!existing) { + networkMap.set(network.ssid, network); + } else { + if (network.active && !existing.active) { + networkMap.set(network.ssid, network); + } else if (!network.active && !existing.active) { + if (network.strength > existing.strength) { + networkMap.set(network.ssid, network); + } + } + } + } + + const networks = Array.from(networkMap.values()); + const rNetworks = root.networks; + + const destroyed = rNetworks.filter(rn => !networks.find(n => + n.frequency === rn.frequency && + n.ssid === rn.ssid && + n.bssid === rn.bssid + )); + for (const network of destroyed) { + const index = rNetworks.indexOf(network); + if (index >= 0) { + rNetworks.splice(index, 1); + network.destroy(); + } + } + + for (const network of networks) { + const match = rNetworks.find(n => + n.frequency === network.frequency && + n.ssid === network.ssid && + n.bssid === network.bssid + ); + if (match) { + match.lastIpcObject = network; + } else { + rNetworks.push(apComp.createObject(root, { + lastIpcObject: network + })); + } + } + + if (callback) callback(root.networks); + checkPendingConnection(); + }); + } + + function getWirelessSSIDs(interfaceName: string, callback: var): void { + let cmd = ["-t", "-f", "SSID,SIGNAL,SECURITY", "device", "wifi", "list"]; + if (interfaceName && interfaceName.length > 0) { + cmd.push("ifname", interfaceName); + } + executeCommand(cmd, (result) => { + if (!result.success) { + if (callback) callback([]); + return; + } + + const ssids = []; + const lines = result.output.trim().split("\n"); + const seenSSIDs = new Set(); + + for (const line of lines) { + if (!line || line.length === 0) continue; + + const parts = line.split(":"); + if (parts.length >= 1) { + const ssid = parts[0].trim(); + if (ssid && ssid.length > 0 && !seenSSIDs.has(ssid)) { + seenSSIDs.add(ssid); + const signalStr = parts.length >= 2 ? parts[1].trim() : ""; + const signal = signalStr ? parseInt(signalStr, 10) : 0; + const security = parts.length >= 3 ? parts[2].trim() : ""; + ssids.push({ + ssid: ssid, + signal: signalStr, + signalValue: isNaN(signal) ? 0 : signal, + security: security + }); + } + } + } + + ssids.sort((a, b) => { + return b.signalValue - a.signalValue; + }); + + if (callback) callback(ssids); + }); + } + + component CommandProcess: Process { + id: proc + property var callback: null + property list command: [] + property bool callbackCalled: false + property int exitCode: 0 + signal processFinished + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stdout: StdioCollector { + id: stdoutCollector + onStreamFinished: { + } + } + + stderr: StdioCollector { + id: stderrCollector + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + + if (isConnectionCommand && root.pendingConnection && root.pendingConnection.callback) { + const needsPassword = (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + } + } + } + } + } + + onExited: { + proc.exitCode = exitCode; + Qt.callLater(() => { + if (proc.callbackCalled) { + proc.processFinished(); + return; + } + + if (proc.callback) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; + const success = proc.exitCode === 0; + + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + const needsPassword = isConnectionCommand && error && error.length > 0 && + (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + proc.processFinished(); + return; + } else if (!success && isConnectionCommand && root.pendingConnection) { + const failedSsid = root.pendingConnection.ssid; + root.connectionFailed(failedSsid); + } + + proc.callbackCalled = true; + proc.callback({ + success: success, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: needsPassword || false + }); + proc.processFinished(); + } else { + proc.processFinished(); + } + }); + } + } + + Component { + id: commandProc + CommandProcess {} + } + + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + } + + Component { + id: apComp + AccessPoint {} + } + + Timer { + id: connectionCheckTimer + interval: 4000 + onTriggered: { + if (root.pendingConnection) { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (!connected && root.pendingConnection.callback) { + let foundPasswordError = false; + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + + if (isConnectionCommand) { + const needsPassword = (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + const pending = root.pendingConnection; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + foundPasswordError = true; + break; + } + } + } + } + } + + if (!foundPasswordError) { + const pending = root.pendingConnection; + const failedSsid = pending.ssid; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.connectionFailed(failedSsid); + pending.callback({ + success: false, + output: "", + error: "Connection timeout", + exitCode: -1, + needsPassword: false + }); + } + } else if (connected) { + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + } + + Timer { + id: immediateCheckTimer + interval: 500 + repeat: true + triggeredOnStart: false + property int checkCount: 0 + + onTriggered: { + if (root.pendingConnection) { + checkCount++; + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + } + root.pendingConnection = null; + } else { + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + + if (isConnectionCommand) { + const needsPassword = (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return; + } + } + } + } + } + + if (checkCount >= 6) { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } else { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + + function checkPendingConnection(): void { + if (root.pendingConnection) { + Qt.callLater(() => { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + } + root.pendingConnection = null; + } else { + if (!immediateCheckTimer.running) { + immediateCheckTimer.start(); + } + } + }); + } + } + + function cidrToSubnetMask(cidr: string): string { + const cidrNum = parseInt(cidr, 10); + if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { + return ""; + } + + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; + const octet1 = (mask >>> 24) & 0xff; + const octet2 = (mask >>> 16) & 0xff; + const octet3 = (mask >>> 8) & 0xff; + const octet4 = mask & 0xff; + + return `${octet1}.${octet2}.${octet3}.${octet4}`; + } + + function getWirelessDeviceDetails(interfaceName: string, callback: var): void { + if (!interfaceName || interfaceName.length === 0) { + const activeInterface = root.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeInterface && activeInterface.device) { + interfaceName = activeInterface.device; + } else { + if (callback) callback(null); + return; + } + } + + executeCommand(["device", "show", interfaceName], (result) => { + if (!result.success || !result.output) { + root.wirelessDeviceDetails = null; + if (callback) callback(null); + return; + } + + const details = parseDeviceDetails(result.output, false); + root.wirelessDeviceDetails = details; + if (callback) callback(details); + }); + } + + function getEthernetDeviceDetails(interfaceName: string, callback: var): void { + if (!interfaceName || interfaceName.length === 0) { + const activeInterface = root.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeInterface && activeInterface.device) { + interfaceName = activeInterface.device; + } else { + if (callback) callback(null); + return; + } + } + + executeCommand(["device", "show", interfaceName], (result) => { + if (!result.success || !result.output) { + root.ethernetDeviceDetails = null; + if (callback) callback(null); + return; + } + + const details = parseDeviceDetails(result.output, true); + root.ethernetDeviceDetails = details; + if (callback) callback(details); + }); + } + + function parseDeviceDetails(output: string, isEthernet: bool): var { + const details = { + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }; + + if (!output || output.length === 0) { + return details; + } + + const lines = output.trim().split("\n"); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(":"); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join(":").trim(); + + if (key.startsWith("IP4.ADDRESS")) { + const ipParts = value.split("/"); + details.ipAddress = ipParts[0] || ""; + if (ipParts[1]) { + details.subnet = cidrToSubnetMask(ipParts[1]); + } else { + details.subnet = ""; + } + } else if (key === "IP4.GATEWAY") { + if (value !== "--") { + details.gateway = value; + } + } else if (key.startsWith("IP4.DNS")) { + if (value !== "--" && value.length > 0) { + details.dns.push(value); + } + } else if (isEthernet && key === "WIRED-PROPERTIES.MAC") { + details.macAddress = value; + } else if (isEthernet && key === "WIRED-PROPERTIES.SPEED") { + details.speed = value; + } else if (!isEthernet && key === "GENERAL.HWADDR") { + details.macAddress = value; + } + } + } + + return details; + } + + Process { + id: monitorProc + running: true + command: ["nmcli", "monitor"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stdout: SplitParser { + onRead: { + log("Connection state change detected, refreshing..."); + root.refreshOnConnectionChange(); + } + } + + onExited: { + log("Monitor process exited, restarting..."); + Qt.callLater(() => { + monitorProc.running = true; + }, 2000); + } + } + + function refreshOnConnectionChange(): void { + getNetworks((networks) => { + const newActive = root.active; + + if (newActive && newActive.active) { + Qt.callLater(() => { + if (root.wirelessInterfaces.length > 0) { + const activeWireless = root.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeWireless && activeWireless.device) { + getWirelessDeviceDetails(activeWireless.device, () => {}); + } + } + + if (root.ethernetInterfaces.length > 0) { + const activeEthernet = root.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeEthernet && activeEthernet.device) { + getEthernetDeviceDetails(activeEthernet.device, () => {}); + } + } + }, 500); + } else { + root.wirelessDeviceDetails = null; + root.ethernetDeviceDetails = null; + } + + getWirelessInterfaces(() => {}); + getEthernetInterfaces(() => { + if (root.activeEthernet && root.activeEthernet.connected) { + Qt.callLater(() => { + getEthernetDeviceDetails(root.activeEthernet.interface, () => {}); + }, 500); + } + }); + }); + } + + Component.onCompleted: { + getWifiStatus(() => {}); + getNetworks(() => {}); + loadSavedConnections(() => {}); + getEthernetInterfaces(() => {}); + + Qt.callLater(() => { + if (root.wirelessInterfaces.length > 0) { + const activeWireless = root.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeWireless && activeWireless.device) { + getWirelessDeviceDetails(activeWireless.device, () => {}); + } + } + + if (root.ethernetInterfaces.length > 0) { + const activeEthernet = root.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeEthernet && activeEthernet.device) { + getEthernetDeviceDetails(activeEthernet.device, () => {}); + } + } + }, 2000); + } +} + -- cgit v1.2.3-freya From 36a91213b14f0dfd000761aa0e7be76db0609101 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 19:05:23 -0500 Subject: network: migrated to nmcli.qml --- modules/bar/popouts/Network.qml | 33 +++++----- plan.md | 137 ---------------------------------------- services/Nmcli.qml | 33 ++++++++++ 3 files changed, 49 insertions(+), 154 deletions(-) delete mode 100644 plan.md diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index f040b6a..cb012bf 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -20,27 +20,27 @@ ColumnLayout { StyledText { Layout.topMargin: Appearance.padding.normal Layout.rightMargin: Appearance.padding.small - text: qsTr("Wifi %1").arg(Network.wifiEnabled ? "enabled" : "disabled") + text: qsTr("Wifi %1").arg(Nmcli.wifiEnabled ? "enabled" : "disabled") font.weight: 500 } Toggle { label: qsTr("Enabled") - checked: Network.wifiEnabled - toggle.onToggled: Network.enableWifi(checked) + checked: Nmcli.wifiEnabled + toggle.onToggled: Nmcli.enableWifi(checked) } StyledText { Layout.topMargin: Appearance.spacing.small Layout.rightMargin: Appearance.padding.small - text: qsTr("%1 networks available").arg(Network.networks.length) + text: qsTr("%1 networks available").arg(Nmcli.networks.length) color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } Repeater { model: ScriptModel { - values: [...Network.networks].sort((a, b) => { + values: [...Nmcli.networks].sort((a, b) => { if (a.active !== b.active) return b.active - a.active; return b.strength - a.strength; @@ -50,7 +50,7 @@ ColumnLayout { RowLayout { id: networkItem - required property Network.AccessPoint modelData + required property Nmcli.AccessPoint modelData readonly property bool isConnecting: root.connectingToSsid === modelData.ssid readonly property bool loading: networkItem.isConnecting @@ -111,14 +111,14 @@ ColumnLayout { StateLayer { color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: networkItem.loading || !Network.wifiEnabled + disabled: networkItem.loading || !Nmcli.wifiEnabled function onClicked(): void { if (networkItem.modelData.active) { - Network.disconnectFromNetwork(); + Nmcli.disconnectFromNetwork(); } else { root.connectingToSsid = networkItem.modelData.ssid; - Network.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); + Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); } } } @@ -151,10 +151,10 @@ ColumnLayout { StateLayer { color: Colours.palette.m3onPrimaryContainer - disabled: Network.scanning || !Network.wifiEnabled + disabled: Nmcli.scanning || !Nmcli.wifiEnabled function onClicked(): void { - Network.rescanWifi(); + Nmcli.rescanWifi(); } } @@ -163,7 +163,7 @@ ColumnLayout { anchors.centerIn: parent spacing: Appearance.spacing.small - opacity: Network.scanning ? 0 : 1 + opacity: Nmcli.scanning ? 0 : 1 MaterialIcon { id: scanIcon @@ -188,22 +188,21 @@ ColumnLayout { strokeWidth: Appearance.padding.small / 2 bgColour: "transparent" implicitHeight: parent.implicitHeight - Appearance.padding.smaller * 2 - running: Network.scanning + running: Nmcli.scanning } } - // Reset connecting state when network changes Connections { - target: Network + target: Nmcli function onActiveChanged(): void { - if (Network.active && root.connectingToSsid === Network.active.ssid) { + if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { root.connectingToSsid = ""; } } function onScanningChanged(): void { - if (!Network.scanning) + if (!Nmcli.scanning) scanIcon.rotation = 0; } } diff --git a/plan.md b/plan.md deleted file mode 100644 index 4762ef0..0000000 --- a/plan.md +++ /dev/null @@ -1,137 +0,0 @@ -# Nmcli.qml Feature Completion Plan - -This document outlines the missing features needed in `Nmcli.qml` to replace `Network.qml` or rewrite the wireless panel in the control center. - -## Current Status - -`Nmcli.qml` currently has: -- ✅ Device status queries -- ✅ Wireless/Ethernet interface listing -- ✅ Interface connection status checking -- ✅ Basic wireless connection (SSID + password) -- ✅ Disconnect functionality -- ✅ Device details (basic) -- ✅ Interface up/down -- ✅ WiFi scanning -- ✅ SSID listing with signal/security (sorted) - -## Missing Features - -### 1. WiFi Radio Control -- [x] `enableWifi(enabled: bool)` - Turn WiFi radio on/off -- [x] `toggleWifi()` - Toggle WiFi radio state -- [x] `wifiEnabled` property - Current WiFi radio state -- [x] Monitor WiFi radio state changes - -**Implementation Notes:** -- Use `nmcli radio wifi on/off` -- Monitor state with `nmcli radio wifi` -- Update `wifiEnabled` property on state changes - -### 2. Network List Management -- [x] `networks` property - List of AccessPoint objects -- [x] `active` property - Currently active network -- [x] Real-time network list updates -- [x] Network grouping by SSID with signal prioritization -- [x] AccessPoint component/object with properties: - - `ssid`, `bssid`, `strength`, `frequency`, `active`, `security`, `isSecure` - -**Implementation Notes:** -- Use `nmcli -g ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY d w` -- Parse and group networks by SSID -- Prioritize active/connected networks -- Update network list on connection changes - -### 3. Connection Management - BSSID Support -- [x] BSSID support in `connectWireless()` function -- [x] Connection profile creation with BSSID (`createConnectionWithPassword`) -- [x] Handle BSSID in connection commands - -**Implementation Notes:** -- Use `nmcli connection add` with `802-11-wireless.bssid` for BSSID-based connections -- Fallback to SSID-only connection if BSSID not available -- Handle existing connection profiles when BSSID is provided - -### 4. Saved Connection Profile Management -- [x] `savedConnections` property - List of saved connection names -- [x] `savedConnectionSsids` property - List of saved SSIDs -- [x] `hasSavedProfile(ssid: string)` function - Check if profile exists -- [x] `forgetNetwork(ssid: string)` function - Delete connection profile -- [x] Load saved connections on startup -- [x] Update saved connections list after connection changes - -**Implementation Notes:** -- Use `nmcli -t -f NAME,TYPE connection show` to list connections -- Query SSIDs for WiFi connections: `nmcli -t -f 802-11-wireless.ssid connection show ` -- Use `nmcli connection delete ` to forget networks -- Case-insensitive SSID matching - -### 5. Pending Connection Tracking -- [x] `pendingConnection` property - Track connection in progress -- [x] Connection state tracking with timers -- [x] Connection success/failure detection -- [x] Automatic retry or callback on failure - -**Implementation Notes:** -- Track pending connection with SSID/BSSID -- Use timers to check connection status -- Monitor network list updates to detect successful connection -- Handle connection failures and trigger callbacks - -### 6. Connection Failure Handling -- [x] `connectionFailed(ssid: string)` signal -- [x] Password requirement detection from error messages -- [x] Connection retry logic -- [x] Error message parsing and reporting - -**Implementation Notes:** -- Parse stderr output for password requirements -- Detect specific error patterns (e.g., "Secrets were required") -- Emit signals for UI to handle password dialogs -- Provide meaningful error messages - -### 7. Password Callback Handling -- [x] `connectToNetworkWithPasswordCheck()` function -- [x] Try connection without password first (use saved password) -- [x] Callback on password requirement -- [x] Handle both secure and open networks - -**Implementation Notes:** -- Attempt connection without password for secure networks -- If connection fails with password error, trigger callback -- For open networks, connect directly -- Support callback pattern for password dialogs - -### 8. Device Details Parsing -- [x] Full parsing of `device show` output -- [x] `wirelessDeviceDetails` property with: - - `ipAddress`, `gateway`, `dns[]`, `subnet`, `macAddress` -- [x] `ethernetDeviceDetails` property with: - - `ipAddress`, `gateway`, `dns[]`, `subnet`, `macAddress`, `speed` -- [x] `cidrToSubnetMask()` helper function -- [x] Update device details on connection changes - -**Implementation Notes:** -- Parse `nmcli device show ` output -- Extract IP4.ADDRESS, IP4.GATEWAY, IP4.DNS, etc. -- Convert CIDR notation to subnet mask -- Handle both wireless and ethernet device details - -### 9. Connection Status Monitoring -- [x] Automatic network list refresh on connection changes -- [x] Monitor connection state changes -- [x] Update active network property -- [x] Refresh device details on connection - -**Implementation Notes:** -- Use Process stdout SplitParser to monitor changes -- Trigger network list refresh on connection events -- Update `active` property when connection changes -- Refresh device details when connected - -### 10. Ethernet Device Management -- [x] `ethernetDevices` property - List of ethernet devices -- [x] `activeEthernet` property - Currently active ethernet device -- [x] `connectEthernet(connectionName, interfaceName)` function -- [x] `disconnectEthernet(connectionName)` function -- [x] Ethernet device details parsing diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 4e45b41..5fb0c6c 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -14,6 +14,7 @@ Singleton { property string activeInterface: "" property string activeConnection: "" property bool wifiEnabled: true + readonly property bool scanning: rescanProc.running readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property list savedConnections: [] @@ -235,6 +236,10 @@ Singleton { } } + function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { + connectWireless(ssid, password, bssid, callback); + } + function connectWireless(ssid: string, password: string, bssid: string, callback: var, retryCount: int): void { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; const retries = retryCount !== undefined ? retryCount : 0; @@ -473,6 +478,22 @@ Singleton { } } + function disconnectFromNetwork(): void { + if (active && active.ssid) { + executeCommand(["connection", "down", active.ssid], (result) => { + if (result.success) { + getNetworks(() => {}); + } + }); + } else { + executeCommand(["device", "disconnect", "wifi"], (result) => { + if (result.success) { + getNetworks(() => {}); + } + }); + } + } + function getDeviceDetails(interfaceName: string, callback: var): void { executeCommand(["device", "show", interfaceName], (result) => { if (callback) callback(result.output); @@ -543,6 +564,10 @@ Singleton { }); } + function rescanWifi(): void { + rescanProc.running = true; + } + function enableWifi(enabled: bool, callback: var): void { const cmd = enabled ? "on" : "off"; executeCommand(["radio", "wifi", cmd], (result) => { @@ -1152,6 +1177,14 @@ Singleton { return details; } + Process { + id: rescanProc + command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"] + onExited: { + getNetworks(() => {}); + } + } + Process { id: monitorProc running: true -- cgit v1.2.3-freya From 7799ec67fda75d8b06f59c491ef119b15d9daf3d Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 19:52:24 -0500 Subject: nmcli: migrated all of wireless controlcenter --- modules/controlcenter/network/WirelessDetails.qml | 61 +++++++++----------- modules/controlcenter/network/WirelessList.qml | 55 +++++++++--------- .../network/WirelessPasswordDialog.qml | 66 ++++++++-------------- modules/controlcenter/network/WirelessSettings.qml | 12 ++-- 4 files changed, 84 insertions(+), 110 deletions(-) diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index d5abc9d..4f11589 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -28,28 +28,23 @@ Item { } function checkSavedProfile(): void { - // Refresh saved connections list to ensure it's up to date - // This ensures the "Forget Network" button visibility is accurate if (network && network.ssid) { - // Always refresh to ensure we have the latest saved connections - // This is important when networks are selected or changed - Network.listConnectionsProc.running = true; + Nmcli.loadSavedConnections(() => {}); } } Connections { - target: Network + target: Nmcli function onActiveChanged() { updateDeviceDetails(); } } function updateDeviceDetails(): void { - // Only update details if the selected network is currently active - if (network && Network.active && Network.active.ssid === network.ssid) { - Network.updateWirelessDeviceDetails(); + if (network && Nmcli.active && Nmcli.active.ssid === network.ssid) { + Nmcli.getWirelessDeviceDetails("", () => {}); } else { - Network.wirelessDeviceDetails = null; + Nmcli.wirelessDeviceDetails = null; } } @@ -84,7 +79,7 @@ Item { if (checked) { handleConnect(); } else { - Network.disconnectFromNetwork(); + Nmcli.disconnectFromNetwork(); } } } @@ -96,8 +91,7 @@ Item { if (!root.network || !root.network.ssid) { return false; } - // Check if profile exists - this will update reactively when savedConnectionSsids changes - return Network.hasSavedProfile(root.network.ssid); + return Nmcli.hasSavedProfile(root.network.ssid); } color: Colours.palette.m3errorContainer onColor: Colours.palette.m3onErrorContainer @@ -105,12 +99,10 @@ Item { onClicked: { if (root.network && root.network.ssid) { - // Disconnect first if connected if (root.network.active) { - Network.disconnectFromNetwork(); + Nmcli.disconnectFromNetwork(); } - // Delete the connection profile - Network.forgetNetwork(root.network.ssid); + Nmcli.forgetNetwork(root.network.ssid, () => {}); } } } @@ -161,7 +153,7 @@ Item { SectionContainer { ConnectionInfoSection { - deviceDetails: Network.wirelessDeviceDetails + deviceDetails: Nmcli.wirelessDeviceDetails } } @@ -169,9 +161,8 @@ Item { } function handleConnect(): void { - // If already connected to a different network, disconnect first - if (Network.active && Network.active.ssid !== root.network.ssid) { - Network.disconnectFromNetwork(); + if (Nmcli.active && Nmcli.active.ssid !== root.network.ssid) { + Nmcli.disconnectFromNetwork(); Qt.callLater(() => { connectToNetwork(); }); @@ -182,29 +173,31 @@ Item { function connectToNetwork(): void { if (root.network.isSecure) { - // Check if we have a saved connection profile for this network (by SSID) - const hasSavedProfile = Network.hasSavedProfile(root.network.ssid); + const hasSavedProfile = Nmcli.hasSavedProfile(root.network.ssid); if (hasSavedProfile) { - // Try connecting with saved password - don't show dialog if it fails - // The saved password should work, but if connection fails for other reasons, - // we'll let the user try manually later - Network.connectToNetwork(root.network.ssid, "", root.network.bssid, null); + Nmcli.connectToNetwork(root.network.ssid, "", root.network.bssid, null); } else { - // No saved profile, try connecting without password first - Network.connectToNetworkWithPasswordCheck( + Nmcli.connectToNetworkWithPasswordCheck( root.network.ssid, root.network.isSecure, - () => { - // Callback: connection failed, show password dialog - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = root.network; + (result) => { + if (result.needsPassword) { + if (Nmcli.pendingConnection) { + Nmcli.connectionCheckTimer.stop(); + Nmcli.immediateCheckTimer.stop(); + Nmcli.immediateCheckTimer.checkCount = 0; + Nmcli.pendingConnection = null; + } + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = root.network; + } }, root.network.bssid ); } } else { - Network.connectToNetwork(root.network.ssid, "", root.network.bssid, null); + Nmcli.connectToNetwork(root.network.ssid, "", root.network.bssid, null); } } } \ No newline at end of file diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index ca6947a..f4e76b4 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -31,22 +31,22 @@ ColumnLayout { } ToggleButton { - toggled: Network.wifiEnabled + toggled: Nmcli.wifiEnabled icon: "wifi" accent: "Tertiary" onClicked: { - Network.toggleWifi(); + Nmcli.toggleWifi(null); } } ToggleButton { - toggled: Network.scanning + toggled: Nmcli.scanning icon: "wifi_find" accent: "Secondary" onClicked: { - Network.rescanWifi(); + Nmcli.rescanWifi(); } } @@ -70,13 +70,13 @@ ColumnLayout { spacing: Appearance.spacing.small StyledText { - text: qsTr("Networks (%1)").arg(Network.networks.length) + text: qsTr("Networks (%1)").arg(Nmcli.networks.length) font.pointSize: Appearance.font.size.large font.weight: 500 } StyledText { - visible: Network.scanning + visible: Nmcli.scanning text: qsTr("Scanning...") color: Colours.palette.m3primary font.pointSize: Appearance.font.size.small @@ -94,7 +94,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - model: Network.networks + model: Nmcli.networks spacing: Appearance.spacing.small / 2 clip: true @@ -183,7 +183,7 @@ ColumnLayout { StateLayer { function onClicked(): void { if (modelData.active) { - Network.disconnectFromNetwork(); + Nmcli.disconnectFromNetwork(); } else { handleConnect(modelData); } @@ -205,19 +205,14 @@ ColumnLayout { } function checkSavedProfileForNetwork(ssid: string): void { - // Refresh saved connections list to ensure it's up to date - // This ensures accurate profile detection when selecting networks if (ssid && ssid.length > 0) { - // Always refresh to ensure we have the latest saved connections - // This is important when a network is selected from the list - Network.listConnectionsProc.running = true; + Nmcli.loadSavedConnections(() => {}); } } function handleConnect(network): void { - // If already connected to a different network, disconnect first - if (Network.active && Network.active.ssid !== network.ssid) { - Network.disconnectFromNetwork(); + if (Nmcli.active && Nmcli.active.ssid !== network.ssid) { + Nmcli.disconnectFromNetwork(); Qt.callLater(() => { connectToNetwork(network); }); @@ -228,29 +223,31 @@ ColumnLayout { function connectToNetwork(network): void { if (network.isSecure) { - // Check if we have a saved connection profile for this network (by SSID) - const hasSavedProfile = Network.hasSavedProfile(network.ssid); + const hasSavedProfile = Nmcli.hasSavedProfile(network.ssid); if (hasSavedProfile) { - // Try connecting with saved password - don't show dialog if it fails - // The saved password should work, but if connection fails for other reasons, - // we'll let the user try manually later - Network.connectToNetwork(network.ssid, "", network.bssid, null); + Nmcli.connectToNetwork(network.ssid, "", network.bssid, null); } else { - // No saved profile, try connecting without password first - Network.connectToNetworkWithPasswordCheck( + Nmcli.connectToNetworkWithPasswordCheck( network.ssid, network.isSecure, - () => { - // Callback: connection failed, show password dialog - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = network; + (result) => { + if (result.needsPassword) { + if (Nmcli.pendingConnection) { + Nmcli.connectionCheckTimer.stop(); + Nmcli.immediateCheckTimer.stop(); + Nmcli.immediateCheckTimer.checkCount = 0; + Nmcli.pendingConnection = null; + } + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = network; + } }, network.bssid ); } } else { - Network.connectToNetwork(network.ssid, "", network.bssid, null); + Nmcli.connectToNetwork(network.ssid, "", network.bssid, null); } } } \ No newline at end of file diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 8a71fa8..e416016 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -108,26 +108,16 @@ Item { Layout.alignment: Qt.AlignHCenter Layout.topMargin: Appearance.spacing.small - visible: Network.connectionStatus.length > 0 || connectButton.connecting + visible: connectButton.connecting text: { - if (Network.connectionStatus.length > 0) { - return Network.connectionStatus; - } else if (connectButton.connecting) { + if (connectButton.connecting) { return qsTr("Connecting..."); } return ""; } - color: { - const status = Network.connectionStatus; - if (status.includes("Error") || status.includes("error") || status.includes("failed")) { - return Colours.palette.m3error; - } else if (status.includes("successful") || status.includes("Connected") || status.includes("success")) { - return Colours.palette.m3primary; - } - return Colours.palette.m3onSurfaceVariant; - } + color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small - font.weight: (Network.connectionStatus.includes("Error") || Network.connectionStatus.includes("error")) ? 500 : 400 + font.weight: 400 wrapMode: Text.WordWrap Layout.maximumWidth: parent.width - Appearance.padding.large * 2 } @@ -166,7 +156,6 @@ Item { if (root.visible) { passwordField.forceActiveFocus(); passwordField.text = ""; - Network.clearConnectionStatus(); } } } @@ -225,10 +214,9 @@ Item { connecting = true; enabled = false; text = qsTr("Connecting..."); - Network.clearConnectionStatus(); // Connect to network - Network.connectToNetwork( + Nmcli.connectToNetwork( root.network.ssid, password, root.network.bssid || "", @@ -248,27 +236,17 @@ Item { return; } - // Check connection status message for success indicators - const status = Network.connectionStatus; - const statusLower = status.toLowerCase(); - - // Check for success indicators in status message - const hasSuccessIndicator = statusLower.includes("connection activated") || - statusLower.includes("successfully") || - statusLower.includes("connected successfully") || - (statusLower.includes("connected") && !statusLower.includes("error") && !statusLower.includes("failed")); - // Check if we're connected to the target network (case-insensitive SSID comparison) - const isConnected = root.network && Network.active && Network.active.ssid && - Network.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && + Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - if (isConnected || hasSuccessIndicator) { + if (isConnected) { // Successfully connected - give it a moment for network list to update Qt.callLater(() => { // Double-check connection is still active - if (root.visible && Network.active && Network.active.ssid) { - const stillConnected = Network.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - if (stillConnected || hasSuccessIndicator) { + if (root.visible && Nmcli.active && Nmcli.active.ssid) { + const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected) { connectionMonitor.stop(); connectButton.connecting = false; connectButton.text = qsTr("Connect"); @@ -279,11 +257,10 @@ Item { return; } - // Check for connection errors (but not warnings about duplicate names) - if (status.includes("Error") || (status.includes("error") && !status.includes("Warning"))) { - // Only treat as error if it's not just a warning about duplicate names - if (!status.includes("another connection with the name") && !status.includes("Reference the connection by its uuid")) { - // Connection failed + // Check for connection failures - if pending connection was cleared but we're not connected + if (Nmcli.pendingConnection === null && connectButton.connecting) { + // Wait a bit more before giving up (allow time for connection to establish) + if (connectionMonitor.repeatCount > 10) { connectionMonitor.stop(); connectButton.connecting = false; connectButton.enabled = true; @@ -297,14 +274,22 @@ Item { interval: 1000 repeat: true triggeredOnStart: false + property int repeatCount: 0 onTriggered: { + repeatCount++; checkConnectionStatus(); } + + onRunningChanged: { + if (!running) { + repeatCount = 0; + } + } } Connections { - target: Network + target: Nmcli function onActiveChanged() { if (root.visible) { checkConnectionStatus(); @@ -318,6 +303,5 @@ Item { connectButton.connecting = false; connectButton.text = qsTr("Connect"); connectionMonitor.stop(); - Network.clearConnectionStatus(); } -} \ No newline at end of file +} diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml index 073b09c..0eb1578 100644 --- a/modules/controlcenter/network/WirelessSettings.qml +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -39,9 +39,9 @@ ColumnLayout { SectionContainer { ToggleRow { label: qsTr("WiFi enabled") - checked: Network.wifiEnabled + checked: Nmcli.wifiEnabled toggle.onToggled: { - Network.enableWifi(checked); + Nmcli.enableWifi(checked); } } } @@ -57,25 +57,25 @@ ColumnLayout { PropertyRow { label: qsTr("Connected network") - value: Network.active ? Network.active.ssid : qsTr("Not connected") + value: Nmcli.active ? Nmcli.active.ssid : qsTr("Not connected") } PropertyRow { showTopMargin: true label: qsTr("Signal strength") - value: Network.active ? qsTr("%1%").arg(Network.active.strength) : qsTr("N/A") + value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A") } PropertyRow { showTopMargin: true label: qsTr("Security") - value: Network.active ? (Network.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") + value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") } PropertyRow { showTopMargin: true label: qsTr("Frequency") - value: Network.active ? qsTr("%1 MHz").arg(Network.active.frequency) : qsTr("N/A") + value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A") } } } \ No newline at end of file -- cgit v1.2.3-freya From 2bb9cd068fe64ab82215a8bc24d82b2a631b2e95 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 19:56:44 -0500 Subject: nmcli: migrated all of ethernet controlcenter --- modules/controlcenter/ethernet/EthernetDetails.qml | 13 ++++++------- modules/controlcenter/ethernet/EthernetList.qml | 9 ++++----- modules/controlcenter/ethernet/EthernetSettings.qml | 4 ++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/modules/controlcenter/ethernet/EthernetDetails.qml b/modules/controlcenter/ethernet/EthernetDetails.qml index d9a004b..68510da 100644 --- a/modules/controlcenter/ethernet/EthernetDetails.qml +++ b/modules/controlcenter/ethernet/EthernetDetails.qml @@ -18,15 +18,15 @@ Item { Component.onCompleted: { if (device && device.interface) { - Network.updateEthernetDeviceDetails(device.interface); + Nmcli.getEthernetDeviceDetails(device.interface, () => {}); } } onDeviceChanged: { if (device && device.interface) { - Network.updateEthernetDeviceDetails(device.interface); + Nmcli.getEthernetDeviceDetails(device.interface, () => {}); } else { - Network.ethernetDeviceDetails = null; + Nmcli.ethernetDeviceDetails = null; } } @@ -59,11 +59,10 @@ Item { checked: root.device?.connected ?? false toggle.onToggled: { if (checked) { - // Use connection name if available, otherwise use interface - Network.connectEthernet(root.device?.connection || "", root.device?.interface || ""); + Nmcli.connectEthernet(root.device?.connection || "", root.device?.interface || "", () => {}); } else { if (root.device?.connection) { - Network.disconnectEthernet(root.device.connection); + Nmcli.disconnectEthernet(root.device.connection, () => {}); } } } @@ -103,7 +102,7 @@ Item { SectionContainer { ConnectionInfoSection { - deviceDetails: Network.ethernetDeviceDetails + deviceDetails: Nmcli.ethernetDeviceDetails } } diff --git a/modules/controlcenter/ethernet/EthernetList.qml b/modules/controlcenter/ethernet/EthernetList.qml index b8c485b..45c9481 100644 --- a/modules/controlcenter/ethernet/EthernetList.qml +++ b/modules/controlcenter/ethernet/EthernetList.qml @@ -49,7 +49,7 @@ ColumnLayout { spacing: Appearance.spacing.small StyledText { - text: qsTr("Devices (%1)").arg(Network.ethernetDeviceCount || Network.ethernetDevices.length) + text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) font.pointSize: Appearance.font.size.large font.weight: 500 } @@ -66,7 +66,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - model: Network.ethernetDevices + model: Nmcli.ethernetDevices spacing: Appearance.spacing.small / 2 clip: true @@ -145,10 +145,9 @@ ColumnLayout { StateLayer { function onClicked(): void { if (modelData.connected && modelData.connection) { - Network.disconnectEthernet(modelData.connection); + Nmcli.disconnectEthernet(modelData.connection, () => {}); } else { - // Use connection name if available, otherwise use interface - Network.connectEthernet(modelData.connection || "", modelData.interface || ""); + Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); } } } diff --git a/modules/controlcenter/ethernet/EthernetSettings.qml b/modules/controlcenter/ethernet/EthernetSettings.qml index 68a1f61..161492c 100644 --- a/modules/controlcenter/ethernet/EthernetSettings.qml +++ b/modules/controlcenter/ethernet/EthernetSettings.qml @@ -64,7 +64,7 @@ ColumnLayout { } StyledText { - text: qsTr("%1").arg(Network.ethernetDeviceCount || Network.ethernetDevices.length) + text: qsTr("%1").arg(Nmcli.ethernetDevices.length) color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } @@ -75,7 +75,7 @@ ColumnLayout { } StyledText { - text: qsTr("%1").arg(Network.ethernetDevices.filter(d => d.connected).length) + text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } -- cgit v1.2.3-freya From f0ea26a71bb1359479c620ba3d571e129547c912 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 20:52:09 -0500 Subject: controlcenter: removed dev panels --- modules/controlcenter/dev/DevControlCenter.qml | 94 -- modules/controlcenter/dev/DevDebugPane.qml | 1979 ------------------------ modules/controlcenter/dev/DevNavRail.qml | 194 --- modules/controlcenter/dev/DevPanes.qml | 70 - modules/controlcenter/dev/DevSession.qml | 23 - modules/controlcenter/dev/DevWindowFactory.qml | 62 - modules/controlcenter/dev/DevWindowTitle.qml | 53 - modules/controlcenter/dev/DevWirelessPane.qml | 68 - modules/utilities/cards/Toggles.qml | 12 - 9 files changed, 2555 deletions(-) delete mode 100644 modules/controlcenter/dev/DevControlCenter.qml delete mode 100644 modules/controlcenter/dev/DevDebugPane.qml delete mode 100644 modules/controlcenter/dev/DevNavRail.qml delete mode 100644 modules/controlcenter/dev/DevPanes.qml delete mode 100644 modules/controlcenter/dev/DevSession.qml delete mode 100644 modules/controlcenter/dev/DevWindowFactory.qml delete mode 100644 modules/controlcenter/dev/DevWindowTitle.qml delete mode 100644 modules/controlcenter/dev/DevWirelessPane.qml diff --git a/modules/controlcenter/dev/DevControlCenter.qml b/modules/controlcenter/dev/DevControlCenter.qml deleted file mode 100644 index 29592ca..0000000 --- a/modules/controlcenter/dev/DevControlCenter.qml +++ /dev/null @@ -1,94 +0,0 @@ -pragma ComponentBehavior: Bound - -import "." -import ".." -import qs.components -import qs.components.controls -import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts - -Item { - id: root - - required property ShellScreen screen - readonly property int rounding: floating ? 0 : Appearance.rounding.normal - - property alias floating: session.floating - property alias active: session.active - property alias navExpanded: session.navExpanded - - readonly property DevSession session: DevSession { - id: session - - root: root - } - - function close(): void { - } - - implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio - implicitHeight: screen.height * Config.controlCenter.sizes.heightMult - - GridLayout { - anchors.fill: parent - - rowSpacing: 0 - columnSpacing: 0 - rows: root.floating ? 2 : 1 - columns: 2 - - Loader { - Layout.fillWidth: true - Layout.columnSpan: 2 - - asynchronous: true - active: root.floating - visible: active - - sourceComponent: DevWindowTitle { - screen: root.screen - session: root.session - } - } - - StyledRect { - Layout.fillHeight: true - - topLeftRadius: root.rounding - bottomLeftRadius: root.rounding - implicitWidth: navRail.implicitWidth - color: Colours.tPalette.m3surfaceContainer - - CustomMouseArea { - anchors.fill: parent - - function onWheel(event: WheelEvent): void { - if (event.angleDelta.y < 0) - root.session.activeIndex = Math.min(root.session.activeIndex + 1, root.session.panes.length - 1); - else if (event.angleDelta.y > 0) - root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0); - } - } - - DevNavRail { - id: navRail - - screen: root.screen - session: root.session - } - } - - DevPanes { - Layout.fillWidth: true - Layout.fillHeight: true - - topRightRadius: root.rounding - bottomRightRadius: root.rounding - session: root.session - } - } -} - diff --git a/modules/controlcenter/dev/DevDebugPane.qml b/modules/controlcenter/dev/DevDebugPane.qml deleted file mode 100644 index 1150f35..0000000 --- a/modules/controlcenter/dev/DevDebugPane.qml +++ /dev/null @@ -1,1979 +0,0 @@ -pragma ComponentBehavior: Bound - -import "." -import ".." -import qs.components -import qs.components.controls -import qs.components.containers -import qs.components.effects -import qs.config -import qs.services -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts - -Item { - id: root - - required property DevSession session - - anchors.fill: parent - - // Track last failed connection - property string lastFailedSsid: "" - - // Connect to connection failure signal - Connections { - target: Nmcli - function onConnectionFailed(ssid: string) { - root.lastFailedSsid = ssid; - appendLog("Connection failed signal received for: " + ssid); - } - } - - StyledFlickable { - id: flickable - - anchors.fill: parent - anchors.margins: Appearance.padding.large - flickableDirection: Flickable.VerticalFlick - contentWidth: width - contentHeight: contentLayout.implicitHeight - - StyledScrollBar.vertical: StyledScrollBar { - flickable: flickable - } - - ColumnLayout { - id: contentLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Debug Panel") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - // Action Buttons Section - StyledRect { - Layout.fillWidth: true - implicitHeight: buttonsLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: buttonsLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Actions") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - Flow { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - TextButton { - text: qsTr("Clear Log") - onClicked: { - debugOutput.text = ""; - appendLog("Debug log cleared"); - } - } - - TextButton { - text: qsTr("Test Action") - onClicked: { - appendLog("Test action executed at " + new Date().toLocaleTimeString()); - } - } - - TextButton { - text: qsTr("Log Network State") - onClicked: { - appendLog("Network state:"); - appendLog(" Active: " + (root.session.network.active ? "Yes" : "No")); - } - } - - TextButton { - text: qsTr("Get Device Status") - onClicked: { - appendLog("Getting device status..."); - try { - Nmcli.getDeviceStatus((output) => { - if (!output) { - appendLog(" Error: No output received"); - return; - } - appendLog("Device Status:"); - const lines = output.trim().split("\n"); - if (lines.length === 0 || (lines.length === 1 && lines[0].length === 0)) { - appendLog(" No devices found"); - } else { - for (const line of lines) { - if (line.length > 0) { - appendLog(" " + line); - } - } - } - }); - } catch (e) { - appendLog("Error: " + e); - } - } - } - - TextButton { - text: qsTr("Get Wireless Interfaces") - onClicked: { - appendLog("Getting wireless interfaces..."); - Nmcli.getWirelessInterfaces((interfaces) => { - appendLog("Wireless Interfaces: " + interfaces.length); - for (const iface of interfaces) { - appendLog(` ${iface.device}: ${iface.state} (${iface.connection})`); - } - }); - } - } - - TextButton { - text: qsTr("Get Ethernet Interfaces") - onClicked: { - appendLog("Getting ethernet interfaces..."); - Nmcli.getEthernetInterfaces((interfaces) => { - appendLog("Ethernet Interfaces: " + interfaces.length); - for (const iface of interfaces) { - appendLog(` ${iface.device}: ${iface.state} (${iface.connection})`); - } - }); - } - } - - TextButton { - text: qsTr("Refresh Status") - onClicked: { - appendLog("Refreshing connection status..."); - Nmcli.refreshStatus((status) => { - appendLog("Connection Status:"); - appendLog(" Connected: " + (status.connected ? "Yes" : "No")); - appendLog(" Interface: " + (status.interface || "None")); - appendLog(" Connection: " + (status.connection || "None")); - }); - } - } - - TextButton { - text: qsTr("Check Interface") - onClicked: { - appendLog("Checking interface connection status..."); - // Check first wireless interface if available - if (Nmcli.wirelessInterfaces.length > 0) { - const iface = Nmcli.wirelessInterfaces[0].device; - appendLog("Checking: " + iface); - Nmcli.isInterfaceConnected(iface, (connected) => { - appendLog(` ${iface}: ${connected ? "Connected" : "Disconnected"}`); - }); - } else { - appendLog("No wireless interfaces found"); - } - } - } - } - } - } - - // WiFi Radio Control Section - StyledRect { - Layout.fillWidth: true - implicitHeight: wifiRadioLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: wifiRadioLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("WiFi Radio Control") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Status: ") + (Nmcli.wifiEnabled ? qsTr("Enabled") : qsTr("Disabled")) - color: Nmcli.wifiEnabled ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Toggle WiFi") - onClicked: { - appendLog("Toggling WiFi radio..."); - Nmcli.toggleWifi((result) => { - if (result.success) { - appendLog("WiFi radio toggled: " + (Nmcli.wifiEnabled ? "Enabled" : "Disabled")); - } else { - appendLog("Failed to toggle WiFi: " + (result.error || "Unknown error")); - } - }); - } - } - - TextButton { - text: qsTr("Enable") - onClicked: { - appendLog("Enabling WiFi radio..."); - Nmcli.enableWifi(true, (result) => { - if (result.success) { - appendLog("WiFi radio enabled"); - } else { - appendLog("Failed to enable WiFi: " + (result.error || "Unknown error")); - } - }); - } - } - - TextButton { - text: qsTr("Disable") - onClicked: { - appendLog("Disabling WiFi radio..."); - Nmcli.enableWifi(false, (result) => { - if (result.success) { - appendLog("WiFi radio disabled"); - } else { - appendLog("Failed to disable WiFi: " + (result.error || "Unknown error")); - } - }); - } - } - - TextButton { - text: qsTr("Check Status") - onClicked: { - appendLog("Checking WiFi radio status..."); - Nmcli.getWifiStatus((enabled) => { - appendLog("WiFi radio status: " + (enabled ? "Enabled" : "Disabled")); - }); - } - } - } - } - } - - // Network List Management Section - StyledRect { - Layout.fillWidth: true - implicitHeight: networkListLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: networkListLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Network List Management") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Networks: %1").arg(Nmcli.networks.length) - } - - StyledText { - visible: Nmcli.active - text: qsTr("Active: %1").arg(Nmcli.active.ssid) - color: Colours.palette.m3primary - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Refresh Networks") - onClicked: { - appendLog("Refreshing network list..."); - Nmcli.getNetworks((networks) => { - appendLog("Found " + networks.length + " networks"); - if (Nmcli.active) { - appendLog("Active network: " + Nmcli.active.ssid + " (Signal: " + Nmcli.active.strength + "%, Security: " + (Nmcli.active.isSecure ? Nmcli.active.security : "Open") + ")"); - } else { - appendLog("No active network"); - } - }); - } - } - - TextButton { - text: qsTr("List All Networks") - onClicked: { - appendLog("Network list:"); - if (Nmcli.networks.length === 0) { - appendLog(" No networks found"); - } else { - for (let i = 0; i < Nmcli.networks.length; i++) { - const net = Nmcli.networks[i]; - const activeMark = net.active ? " [ACTIVE]" : ""; - appendLog(` ${i + 1}. ${net.ssid}${activeMark}`); - appendLog(` Signal: ${net.strength}%, Freq: ${net.frequency}MHz, Security: ${net.isSecure ? net.security : "Open"}`); - if (net.bssid) { - appendLog(` BSSID: ${net.bssid}`); - } - } - } - } - } - } - } - } - - // Interface Selector Section (for future features) - Item { - Layout.fillWidth: true - implicitHeight: interfaceSelectorContainer.implicitHeight - z: 10 // Ensure dropdown menu appears above other elements - - StyledRect { - id: interfaceSelectorContainer - - anchors.fill: parent - implicitHeight: interfaceSelectorLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: interfaceSelectorLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Interface Selector") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - SplitButton { - id: interfaceSelector - - type: SplitButton.Tonal - fallbackText: qsTr("Select Interface") - fallbackIcon: "settings_ethernet" - menuItems: interfaceList.instances - menuOnTop: true // Position menu above button to avoid being covered - - property string selectedInterface: "" - - menu.onItemSelected: (item) => { - interfaceSelector.selectedInterface = item.modelData.device; - appendLog("Selected interface: " + item.modelData.device + " (" + item.modelData.type + ")"); - } - - Variants { - id: interfaceList - - model: interfaceSelector.interfaces - - MenuItem { - required property var modelData - - text: modelData.device + " (" + modelData.type + ")" - icon: modelData.type === "wifi" ? "wifi" : "settings_ethernet" - } - } - - property list interfaces: [] - - function refreshInterfaces(): void { - appendLog("Refreshing interface list..."); - Nmcli.getAllInterfaces((interfaces) => { - interfaceSelector.interfaces = interfaces; - if (interfaces.length > 0) { - // Wait for Variants to create instances, then set active - Qt.callLater(() => { - if (interfaceList.instances.length > 0) { - interfaceSelector.active = interfaceList.instances[0]; - interfaceSelector.selectedInterface = interfaces[0].device; - } - }); - appendLog("Found " + interfaces.length + " interfaces"); - } else { - interfaceSelector.selectedInterface = ""; - appendLog("No interfaces found"); - } - }); - } - - Component.onCompleted: { - // Ensure menu appears above other elements - menu.z = 100; - } - } - - TextButton { - text: qsTr("Refresh") - onClicked: { - interfaceSelector.refreshInterfaces(); - } - } - - TextButton { - text: qsTr("Up") - enabled: interfaceSelector.selectedInterface.length > 0 - onClicked: { - if (interfaceSelector.selectedInterface) { - appendLog("Bringing interface up: " + interfaceSelector.selectedInterface); - Nmcli.bringInterfaceUp(interfaceSelector.selectedInterface, (result) => { - if (result.success) { - appendLog("Interface up: Success"); - } else { - appendLog("Interface up: Failed (exit code: " + result.exitCode + ")"); - if (result.error && result.error.length > 0) { - appendLog("Error: " + result.error); - } - } - // Refresh interface list after bringing up - Qt.callLater(() => { - interfaceSelector.refreshInterfaces(); - }, 500); - }); - } - } - } - - TextButton { - text: qsTr("Down") - enabled: interfaceSelector.selectedInterface.length > 0 - onClicked: { - if (interfaceSelector.selectedInterface) { - appendLog("Bringing interface down: " + interfaceSelector.selectedInterface); - Nmcli.bringInterfaceDown(interfaceSelector.selectedInterface, (result) => { - if (result.success) { - appendLog("Interface down: Success"); - } else { - appendLog("Interface down: Failed (exit code: " + result.exitCode + ")"); - if (result.error && result.error.length > 0) { - appendLog("Error: " + result.error); - } - } - // Refresh interface list after bringing down - Qt.callLater(() => { - interfaceSelector.refreshInterfaces(); - }, 500); - }); - } - } - } - } - } - } - } - - // Wireless SSID Selector Section - Item { - Layout.fillWidth: true - implicitHeight: ssidSelectorContainer.implicitHeight - z: 10 // Ensure dropdown menu appears above other elements - - StyledRect { - id: ssidSelectorContainer - - anchors.fill: parent - implicitHeight: ssidSelectorLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: ssidSelectorLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Wireless SSID Selector") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - SplitButton { - id: ssidSelector - - type: SplitButton.Tonal - fallbackText: qsTr("Select SSID") - fallbackIcon: "wifi" - menuItems: ssidList.instances - menuOnTop: true - - property string selectedSSID: "" - - menu.onItemSelected: (item) => { - ssidSelector.selectedSSID = item.modelData.ssid; - appendLog("Selected SSID: " + item.modelData.ssid + " (Signal: " + item.modelData.signal + ", Security: " + item.modelData.security + ")"); - } - - Component.onCompleted: { - // Ensure menu appears above other elements - menu.z = 100; - } - - Variants { - id: ssidList - - model: ssidSelector.ssids - - MenuItem { - required property var modelData - - text: modelData.ssid + (modelData.signal ? " (" + modelData.signal + "%)" : "") - icon: "wifi" - } - } - - property list ssids: [] - - function scanForSSIDs(): void { - appendLog("Scanning for wireless networks..."); - // Use first wireless interface if available, or let nmcli choose - let iface = ""; - if (interfaceSelector.selectedInterface) { - // Check if selected interface is wireless - for (const i of interfaceSelector.interfaces) { - if (i.device === interfaceSelector.selectedInterface && i.type === "wifi") { - iface = interfaceSelector.selectedInterface; - break; - } - } - } - - // If no wireless interface selected, use first available - if (!iface && Nmcli.wirelessInterfaces.length > 0) { - iface = Nmcli.wirelessInterfaces[0].device; - } - - Nmcli.scanWirelessNetworks(iface, (scanResult) => { - if (scanResult.success) { - appendLog("Scan completed, fetching SSID list..."); - // Wait a moment for scan results to be available - Qt.callLater(() => { - Nmcli.getWirelessSSIDs(iface, (ssids) => { - ssidSelector.ssids = ssids; - if (ssids.length > 0) { - Qt.callLater(() => { - if (ssidList.instances.length > 0) { - ssidSelector.active = ssidList.instances[0]; - ssidSelector.selectedSSID = ssids[0].ssid; - } - }); - appendLog("Found " + ssids.length + " SSIDs"); - } else { - appendLog("No SSIDs found"); - } - }); - }, 1000); - } else { - appendLog("Scan failed: " + (scanResult.error || "Unknown error")); - } - }); - } - } - - TextButton { - text: qsTr("Scan") - onClicked: { - ssidSelector.scanForSSIDs(); - } - } - } - } - } - } - - // Wireless Connection Test Section - StyledRect { - Layout.fillWidth: true - implicitHeight: connectionTestLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: connectionTestLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Wireless Connection Test") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("SSID: %1").arg(ssidSelector.selectedSSID || "None selected") - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Connect (No Password)") - enabled: ssidSelector.selectedSSID.length > 0 - onClicked: { - if (ssidSelector.selectedSSID) { - appendLog("Connecting to: " + ssidSelector.selectedSSID + " (no password)"); - // Find the network to get BSSID - const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); - const bssid = network ? network.bssid : ""; - Nmcli.connectWireless(ssidSelector.selectedSSID, "", bssid, (result) => { - if (result.success) { - appendLog("Connection succeeded!"); - // Refresh network list after connection - Qt.callLater(() => { - Nmcli.getNetworks(() => {}); - }, 1000); - } else { - appendLog("Connection failed: " + (result.error || "Unknown error")); - // Refresh network list anyway to check status - Qt.callLater(() => { - Nmcli.getNetworks(() => {}); - }, 1000); - } - }); - appendLog("Connection initiated, tracking pending connection..."); - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Password:") - } - - Item { - Layout.fillWidth: true - implicitHeight: passwordField.implicitHeight + Appearance.padding.small * 2 - - StyledRect { - anchors.fill: parent - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - border.width: passwordField.activeFocus ? 2 : 1 - border.color: passwordField.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline - - Behavior on border.color { - CAnim {} - } - } - - StyledTextField { - id: passwordField - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - echoMode: TextField.Password - placeholderText: qsTr("Enter password") - - Keys.onReturnPressed: { - if (connectWithPasswordButton.enabled) { - connectWithPasswordButton.clicked(); - } - } - Keys.onEnterPressed: { - if (connectWithPasswordButton.enabled) { - connectWithPasswordButton.clicked(); - } - } - } - } - - TextButton { - id: connectWithPasswordButton - text: qsTr("Connect") - enabled: ssidSelector.selectedSSID.length > 0 && passwordField.text.length > 0 - onClicked: { - if (ssidSelector.selectedSSID && passwordField.text) { - appendLog("Connecting to: " + ssidSelector.selectedSSID + " (with password)"); - // Find the network to get BSSID - const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); - const bssid = network ? network.bssid : ""; - Nmcli.connectWireless(ssidSelector.selectedSSID, passwordField.text, bssid, (result) => { - if (result.success) { - appendLog("Connection succeeded!"); - // Clear password field - passwordField.text = ""; - // Refresh network list after connection - Qt.callLater(() => { - Nmcli.getNetworks(() => {}); - }, 1000); - } else { - appendLog("Connection failed: " + (result.error || "Unknown error")); - if (result.exitCode !== 0) { - appendLog("Exit code: " + result.exitCode); - } - // Refresh network list anyway to check status - Qt.callLater(() => { - Nmcli.getNetworks(() => {}); - }, 1000); - } - }); - appendLog("Connection initiated, tracking pending connection..."); - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: { - const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); - const bssid = network && network.bssid ? network.bssid : "N/A"; - return qsTr("BSSID: %1").arg(bssid); - } - } - - Item { - Layout.fillWidth: true - } - } - } - } - - // Saved Connection Profiles Section - StyledRect { - Layout.fillWidth: true - implicitHeight: savedProfilesLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: savedProfilesLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Saved Connection Profiles") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Connections: %1").arg(Nmcli.savedConnections.length) - } - - StyledText { - text: qsTr("WiFi SSIDs: %1").arg(Nmcli.savedConnectionSsids.length) - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Refresh") - onClicked: { - appendLog("Refreshing saved connections..."); - Nmcli.loadSavedConnections((ssids) => { - appendLog("Found " + Nmcli.savedConnections.length + " saved connections"); - appendLog("Found " + Nmcli.savedConnectionSsids.length + " WiFi SSIDs"); - }); - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Selected SSID: %1").arg(ssidSelector.selectedSSID || "None") - } - - StyledText { - visible: ssidSelector.selectedSSID.length > 0 - text: { - if (!ssidSelector.selectedSSID) return ""; - const hasProfile = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); - return hasProfile ? qsTr("[Saved Profile]") : qsTr("[Not Saved]"); - } - color: { - if (!ssidSelector.selectedSSID) return Colours.palette.m3onSurface; - const hasProfile = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); - return hasProfile ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant; - } - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Check Profile") - enabled: ssidSelector.selectedSSID.length > 0 - onClicked: { - if (ssidSelector.selectedSSID) { - const hasProfile = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); - appendLog("Profile check for '" + ssidSelector.selectedSSID + "': " + (hasProfile ? "Saved" : "Not saved")); - } - } - } - - TextButton { - text: qsTr("Forget Network") - enabled: ssidSelector.selectedSSID.length > 0 - onClicked: { - if (ssidSelector.selectedSSID) { - appendLog("Forgetting network: " + ssidSelector.selectedSSID); - Nmcli.forgetNetwork(ssidSelector.selectedSSID, (result) => { - if (result.success) { - appendLog("Network forgotten successfully"); - } else { - appendLog("Failed to forget network: " + (result.error || "Unknown error")); - } - }); - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - TextButton { - text: qsTr("List All Saved SSIDs") - onClicked: { - appendLog("Saved WiFi SSIDs:"); - if (Nmcli.savedConnectionSsids.length === 0) { - appendLog(" No saved SSIDs"); - } else { - for (let i = 0; i < Nmcli.savedConnectionSsids.length; i++) { - appendLog(" " + (i + 1) + ". " + Nmcli.savedConnectionSsids[i]); - } - } - } - } - - TextButton { - text: qsTr("List All Connections") - onClicked: { - appendLog("Saved Connections:"); - if (Nmcli.savedConnections.length === 0) { - appendLog(" No saved connections"); - } else { - for (let i = 0; i < Nmcli.savedConnections.length; i++) { - appendLog(" " + (i + 1) + ". " + Nmcli.savedConnections[i]); - } - } - } - } - } - } - } - - // Pending Connection Tracking Section - StyledRect { - Layout.fillWidth: true - implicitHeight: pendingConnectionLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: pendingConnectionLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Pending Connection Tracking") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Status: %1").arg(Nmcli.pendingConnection ? "Connecting..." : "No pending connection") - color: Nmcli.pendingConnection ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - } - - StyledText { - visible: Nmcli.pendingConnection - text: qsTr("SSID: %1").arg(Nmcli.pendingConnection ? Nmcli.pendingConnection.ssid : "") - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Check Status") - onClicked: { - if (Nmcli.pendingConnection) { - appendLog("Pending connection: " + Nmcli.pendingConnection.ssid); - appendLog("BSSID: " + (Nmcli.pendingConnection.bssid || "N/A")); - const connected = Nmcli.active && Nmcli.active.ssid === Nmcli.pendingConnection.ssid; - appendLog("Connected: " + (connected ? "Yes" : "No")); - if (connected) { - appendLog("Connection succeeded!"); - } else { - appendLog("Still connecting..."); - } - } else { - appendLog("No pending connection"); - } - } - } - - TextButton { - text: qsTr("Clear Pending") - enabled: Nmcli.pendingConnection !== null - onClicked: { - if (Nmcli.pendingConnection) { - appendLog("Clearing pending connection: " + Nmcli.pendingConnection.ssid); - Nmcli.pendingConnection = null; - appendLog("Pending connection cleared"); - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Active Network: %1").arg(Nmcli.active ? Nmcli.active.ssid : "None") - color: Nmcli.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Refresh Networks & Check") - onClicked: { - appendLog("Refreshing network list to check pending connection..."); - Nmcli.getNetworks((networks) => { - appendLog("Network list refreshed"); - if (Nmcli.pendingConnection) { - const connected = Nmcli.active && Nmcli.active.ssid === Nmcli.pendingConnection.ssid; - appendLog("Pending connection check: " + (connected ? "Connected!" : "Still connecting...")); - } - }); - } - } - } - } - } - - // Connection Failure Handling Section - StyledRect { - Layout.fillWidth: true - implicitHeight: connectionFailureLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: connectionFailureLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Connection Failure Handling") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Last Failed SSID: %1").arg(lastFailedSsid || "None") - color: lastFailedSsid ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Clear Failure") - enabled: lastFailedSsid.length > 0 - onClicked: { - lastFailedSsid = ""; - appendLog("Cleared failure status"); - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Test Password Detection") - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Test Secure Network (No Password)") - enabled: ssidSelector.selectedSSID.length > 0 - onClicked: { - if (ssidSelector.selectedSSID) { - const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); - if (network && network.isSecure) { - appendLog("Testing connection to secure network without password (should detect password requirement)"); - const bssid = network ? network.bssid : ""; - Nmcli.connectWireless(ssidSelector.selectedSSID, "", bssid, (result) => { - if (result.needsPassword) { - appendLog("✓ Password requirement detected correctly!"); - appendLog("Error: " + (result.error || "N/A")); - } else if (result.success) { - appendLog("Connection succeeded (saved password used)"); - } else { - appendLog("Connection failed: " + (result.error || "Unknown error")); - } - }); - } else { - appendLog("Selected network is not secure, cannot test password detection"); - } - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Connection Retry Test") - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Test Retry Logic") - enabled: ssidSelector.selectedSSID.length > 0 - onClicked: { - if (ssidSelector.selectedSSID) { - appendLog("Testing connection retry logic (will retry up to 2 times on failure)"); - const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); - const bssid = network ? network.bssid : ""; - // Use invalid password to trigger failure - Nmcli.connectWireless(ssidSelector.selectedSSID, "invalid_password_test", bssid, (result) => { - if (result.success) { - appendLog("Connection succeeded (unexpected)"); - } else { - appendLog("Connection failed after retries: " + (result.error || "Unknown error")); - } - }); - } - } - } - } - } - } - - // Password Callback Handling Section - StyledRect { - Layout.fillWidth: true - implicitHeight: passwordCallbackLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: passwordCallbackLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Password Callback Handling") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Selected SSID: %1").arg(ssidSelector.selectedSSID || "None") - } - - StyledText { - visible: ssidSelector.selectedSSID.length > 0 - text: { - if (!ssidSelector.selectedSSID) return ""; - const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); - if (!network) return ""; - return network.isSecure ? qsTr("[Secure]") : qsTr("[Open]"); - } - color: { - if (!ssidSelector.selectedSSID) return Colours.palette.m3onSurface; - const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); - if (!network) return Colours.palette.m3onSurface; - return network.isSecure ? Colours.palette.m3error : Colours.palette.m3primary; - } - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Test Password Check (Secure)") - enabled: ssidSelector.selectedSSID.length > 0 - onClicked: { - if (ssidSelector.selectedSSID) { - const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); - if (network && network.isSecure) { - appendLog("Testing password check for secure network: " + ssidSelector.selectedSSID); - appendLog("This will try saved password first, then prompt if needed"); - const bssid = network ? network.bssid : ""; - Nmcli.connectToNetworkWithPasswordCheck(ssidSelector.selectedSSID, true, (result) => { - if (result.success) { - if (result.usedSavedPassword) { - appendLog("✓ Connection succeeded using saved password!"); - } else { - appendLog("✓ Connection succeeded!"); - } - // Refresh network list - Qt.callLater(() => { - Nmcli.getNetworks(() => {}); - }, 1000); - } else if (result.needsPassword) { - appendLog("→ Password required - callback triggered"); - appendLog(" Error: " + (result.error || "N/A")); - appendLog(" (In real UI, this would show password dialog)"); - } else { - appendLog("✗ Connection failed: " + (result.error || "Unknown error")); - } - }, bssid); - } else { - appendLog("Selected network is not secure, cannot test password check"); - } - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Test Open Network") - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Test Password Check (Open)") - enabled: ssidSelector.selectedSSID.length > 0 - onClicked: { - if (ssidSelector.selectedSSID) { - const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); - if (network && !network.isSecure) { - appendLog("Testing password check for open network: " + ssidSelector.selectedSSID); - appendLog("Open networks should connect directly without password"); - const bssid = network ? network.bssid : ""; - Nmcli.connectToNetworkWithPasswordCheck(ssidSelector.selectedSSID, false, (result) => { - if (result.success) { - appendLog("✓ Connection succeeded!"); - // Refresh network list - Qt.callLater(() => { - Nmcli.getNetworks(() => {}); - }, 1000); - } else { - appendLog("✗ Connection failed: " + (result.error || "Unknown error")); - } - }, bssid); - } else { - appendLog("Selected network is not open, cannot test open network handling"); - } - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Test with Saved Password") - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Test Secure Network (Has Saved Password)") - enabled: ssidSelector.selectedSSID.length > 0 - onClicked: { - if (ssidSelector.selectedSSID) { - const network = Nmcli.networks.find(n => n.ssid === ssidSelector.selectedSSID); - if (network && network.isSecure) { - const hasSaved = Nmcli.hasSavedProfile(ssidSelector.selectedSSID); - appendLog("Testing password check for: " + ssidSelector.selectedSSID); - appendLog("Has saved profile: " + (hasSaved ? "Yes" : "No")); - if (hasSaved) { - appendLog("This should connect using saved password without prompting"); - } else { - appendLog("This should prompt for password since no saved profile exists"); - } - const bssid = network ? network.bssid : ""; - Nmcli.connectToNetworkWithPasswordCheck(ssidSelector.selectedSSID, true, (result) => { - if (result.success) { - if (result.usedSavedPassword) { - appendLog("✓ Connection succeeded using saved password!"); - } else { - appendLog("✓ Connection succeeded!"); - } - // Refresh network list - Qt.callLater(() => { - Nmcli.getNetworks(() => {}); - }, 1000); - } else if (result.needsPassword) { - appendLog("→ Password required - callback triggered"); - appendLog(" (In real UI, this would show password dialog)"); - } else { - appendLog("✗ Connection failed: " + (result.error || "Unknown error")); - } - }, bssid); - } else { - appendLog("Selected network is not secure, cannot test saved password"); - } - } - } - } - } - } - } - - // Device Details Parsing Section - StyledRect { - Layout.fillWidth: true - implicitHeight: deviceDetailsLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: deviceDetailsLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Device Details Parsing") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Wireless Device Details") - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Get Wireless Details") - onClicked: { - const activeInterface = interfaceSelector.selectedInterface; - if (activeInterface && activeInterface.length > 0) { - appendLog("Getting wireless device details for: " + activeInterface); - Nmcli.getWirelessDeviceDetails(activeInterface, (details) => { - if (details) { - appendLog("Wireless Device Details:"); - appendLog(" IP Address: " + (details.ipAddress || "N/A")); - appendLog(" Gateway: " + (details.gateway || "N/A")); - appendLog(" Subnet: " + (details.subnet || "N/A")); - appendLog(" MAC Address: " + (details.macAddress || "N/A")); - appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); - } else { - appendLog("Failed to get wireless device details"); - } - }); - } else { - appendLog("Getting wireless device details for active interface"); - Nmcli.getWirelessDeviceDetails("", (details) => { - if (details) { - appendLog("Wireless Device Details:"); - appendLog(" IP Address: " + (details.ipAddress || "N/A")); - appendLog(" Gateway: " + (details.gateway || "N/A")); - appendLog(" Subnet: " + (details.subnet || "N/A")); - appendLog(" MAC Address: " + (details.macAddress || "N/A")); - appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); - } else { - appendLog("No active wireless interface or failed to get details"); - } - }); - } - } - } - - TextButton { - text: qsTr("Show Current") - onClicked: { - if (Nmcli.wirelessDeviceDetails) { - const details = Nmcli.wirelessDeviceDetails; - appendLog("Current Wireless Device Details:"); - appendLog(" IP Address: " + (details.ipAddress || "N/A")); - appendLog(" Gateway: " + (details.gateway || "N/A")); - appendLog(" Subnet: " + (details.subnet || "N/A")); - appendLog(" MAC Address: " + (details.macAddress || "N/A")); - appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); - } else { - appendLog("No wireless device details available"); - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Ethernet Device Details") - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Get Ethernet Details") - onClicked: { - const activeInterface = interfaceSelector.selectedInterface; - if (activeInterface && activeInterface.length > 0) { - appendLog("Getting ethernet device details for: " + activeInterface); - Nmcli.getEthernetDeviceDetails(activeInterface, (details) => { - if (details) { - appendLog("Ethernet Device Details:"); - appendLog(" IP Address: " + (details.ipAddress || "N/A")); - appendLog(" Gateway: " + (details.gateway || "N/A")); - appendLog(" Subnet: " + (details.subnet || "N/A")); - appendLog(" MAC Address: " + (details.macAddress || "N/A")); - appendLog(" Speed: " + (details.speed || "N/A")); - appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); - } else { - appendLog("Failed to get ethernet device details"); - } - }); - } else { - appendLog("Getting ethernet device details for active interface"); - Nmcli.getEthernetDeviceDetails("", (details) => { - if (details) { - appendLog("Ethernet Device Details:"); - appendLog(" IP Address: " + (details.ipAddress || "N/A")); - appendLog(" Gateway: " + (details.gateway || "N/A")); - appendLog(" Subnet: " + (details.subnet || "N/A")); - appendLog(" MAC Address: " + (details.macAddress || "N/A")); - appendLog(" Speed: " + (details.speed || "N/A")); - appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); - } else { - appendLog("No active ethernet interface or failed to get details"); - } - }); - } - } - } - - TextButton { - text: qsTr("Show Current") - onClicked: { - if (Nmcli.ethernetDeviceDetails) { - const details = Nmcli.ethernetDeviceDetails; - appendLog("Current Ethernet Device Details:"); - appendLog(" IP Address: " + (details.ipAddress || "N/A")); - appendLog(" Gateway: " + (details.gateway || "N/A")); - appendLog(" Subnet: " + (details.subnet || "N/A")); - appendLog(" MAC Address: " + (details.macAddress || "N/A")); - appendLog(" Speed: " + (details.speed || "N/A")); - appendLog(" DNS: " + (details.dns && details.dns.length > 0 ? details.dns.join(", ") : "N/A")); - } else { - appendLog("No ethernet device details available"); - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("CIDR to Subnet Mask Test") - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Test CIDR Conversion") - onClicked: { - appendLog("Testing CIDR to Subnet Mask conversion:"); - const testCases = ["8", "16", "24", "32", "0", "25", "30"]; - for (let i = 0; i < testCases.length; i++) { - const cidr = testCases[i]; - const subnet = Nmcli.cidrToSubnetMask(cidr); - appendLog(" /" + cidr + " -> " + (subnet || "Invalid")); - } - } - } - } - } - } - - // Connection Status Monitoring Section - StyledRect { - Layout.fillWidth: true - implicitHeight: connectionMonitoringLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: connectionMonitoringLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Connection Status Monitoring") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Active Network: %1").arg(Nmcli.active ? Nmcli.active.ssid : "None") - color: Nmcli.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - } - - StyledText { - visible: Nmcli.active - text: Nmcli.active ? qsTr("Signal: %1%").arg(Nmcli.active.strength) : "" - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Refresh Networks") - onClicked: { - appendLog("Manually refreshing network list..."); - Nmcli.getNetworks((networks) => { - appendLog("Network list refreshed: " + networks.length + " networks"); - if (Nmcli.active) { - appendLog("Active network: " + Nmcli.active.ssid); - } else { - appendLog("No active network"); - } - }); - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Monitor Status") - } - - StyledText { - text: qsTr("Monitoring connection changes (automatic refresh enabled)") - color: Colours.palette.m3primary - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Test Connection Change") - onClicked: { - appendLog("Testing connection change detection..."); - appendLog("This will trigger a manual refresh to simulate a connection change"); - Nmcli.refreshOnConnectionChange(); - appendLog("Refresh triggered - check if network list and device details updated"); - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Device Details Auto-Refresh") - } - - StyledText { - text: { - if (Nmcli.wirelessDeviceDetails) { - return qsTr("Wireless: %1").arg(Nmcli.wirelessDeviceDetails.ipAddress || "N/A"); - } else if (Nmcli.ethernetDeviceDetails) { - return qsTr("Ethernet: %1").arg(Nmcli.ethernetDeviceDetails.ipAddress || "N/A"); - } else { - return qsTr("No device details"); - } - } - color: (Nmcli.wirelessDeviceDetails || Nmcli.ethernetDeviceDetails) ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Refresh Device Details") - onClicked: { - appendLog("Manually refreshing device details..."); - if (Nmcli.active && Nmcli.active.active) { - appendLog("Active network detected, refreshing device details..."); - // Refresh wireless device details - if (Nmcli.wirelessInterfaces.length > 0) { - const activeWireless = Nmcli.wirelessInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); - }); - if (activeWireless && activeWireless.device) { - Nmcli.getWirelessDeviceDetails(activeWireless.device, (details) => { - if (details) { - appendLog("Wireless device details refreshed"); - } - }); - } - } - // Refresh ethernet device details - if (Nmcli.ethernetInterfaces.length > 0) { - const activeEthernet = Nmcli.ethernetInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); - }); - if (activeEthernet && activeEthernet.device) { - Nmcli.getEthernetDeviceDetails(activeEthernet.device, (details) => { - if (details) { - appendLog("Ethernet device details refreshed"); - } - }); - } - } - } else { - appendLog("No active network, clearing device details"); - Nmcli.wirelessDeviceDetails = null; - Nmcli.ethernetDeviceDetails = null; - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Connection Events") - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Show Active Network Info") - onClicked: { - if (Nmcli.active) { - appendLog("Active Network Information:"); - appendLog(" SSID: " + Nmcli.active.ssid); - appendLog(" BSSID: " + (Nmcli.active.bssid || "N/A")); - appendLog(" Signal: " + Nmcli.active.strength + "%"); - appendLog(" Frequency: " + Nmcli.active.frequency + " MHz"); - appendLog(" Security: " + (Nmcli.active.security || "Open")); - appendLog(" Is Secure: " + (Nmcli.active.isSecure ? "Yes" : "No")); - } else { - appendLog("No active network"); - } - } - } - } - } - } - - // Ethernet Device Management Section - StyledRect { - Layout.fillWidth: true - implicitHeight: ethernetManagementLayout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: ethernetManagementLayout - - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Ethernet Device Management") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Ethernet Devices: %1").arg(Nmcli.ethernetDevices.length) - } - - StyledText { - text: qsTr("Active: %1").arg(Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : "None") - color: Nmcli.activeEthernet ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Refresh Devices") - onClicked: { - appendLog("Refreshing ethernet devices..."); - Nmcli.getEthernetInterfaces((interfaces) => { - appendLog("Found " + Nmcli.ethernetDevices.length + " ethernet devices"); - for (let i = 0; i < Nmcli.ethernetDevices.length; i++) { - const dev = Nmcli.ethernetDevices[i]; - appendLog(" " + (i + 1) + ". " + dev.interface + " - " + dev.state + (dev.connected ? " [Connected]" : "")); - } - if (Nmcli.activeEthernet) { - appendLog("Active ethernet: " + Nmcli.activeEthernet.interface); - } - }); - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Selected Interface: %1").arg(interfaceSelector.selectedInterface || "None") - } - - StyledText { - visible: interfaceSelector.selectedInterface.length > 0 - text: { - if (!interfaceSelector.selectedInterface) return ""; - const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); - if (!device) return ""; - return device.connected ? qsTr("[Connected]") : qsTr("[Disconnected]"); - } - color: { - if (!interfaceSelector.selectedInterface) return Colours.palette.m3onSurface; - const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); - if (!device) return Colours.palette.m3onSurface; - return device.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant; - } - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Connect Ethernet") - enabled: interfaceSelector.selectedInterface.length > 0 - onClicked: { - if (interfaceSelector.selectedInterface) { - const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); - if (device) { - appendLog("Connecting ethernet: " + interfaceSelector.selectedInterface); - appendLog("Connection name: " + (device.connection || "N/A")); - Nmcli.connectEthernet(device.connection || "", interfaceSelector.selectedInterface, (result) => { - if (result.success) { - appendLog("✓ Ethernet connection initiated"); - appendLog("Refreshing device list..."); - } else { - appendLog("✗ Failed to connect: " + (result.error || "Unknown error")); - } - }); - } else { - appendLog("Device not found in ethernet devices list"); - } - } - } - } - - TextButton { - text: qsTr("Disconnect Ethernet") - enabled: interfaceSelector.selectedInterface.length > 0 - onClicked: { - if (interfaceSelector.selectedInterface) { - const device = Nmcli.ethernetDevices.find(d => d.interface === interfaceSelector.selectedInterface); - if (device && device.connection) { - appendLog("Disconnecting ethernet: " + device.connection); - Nmcli.disconnectEthernet(device.connection, (result) => { - if (result.success) { - appendLog("✓ Ethernet disconnected"); - appendLog("Refreshing device list..."); - } else { - appendLog("✗ Failed to disconnect: " + (result.error || "Unknown error")); - } - }); - } else { - appendLog("No connection name available for this device"); - } - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("List All Ethernet Devices") - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("List Devices") - onClicked: { - appendLog("Ethernet Devices:"); - if (Nmcli.ethernetDevices.length === 0) { - appendLog(" No ethernet devices found"); - } else { - for (let i = 0; i < Nmcli.ethernetDevices.length; i++) { - const dev = Nmcli.ethernetDevices[i]; - appendLog(" " + (i + 1) + ". " + dev.interface); - appendLog(" Type: " + dev.type); - appendLog(" State: " + dev.state); - appendLog(" Connection: " + (dev.connection || "None")); - appendLog(" Connected: " + (dev.connected ? "Yes" : "No")); - } - } - } - } - - TextButton { - text: qsTr("Show Active Device") - onClicked: { - if (Nmcli.activeEthernet) { - appendLog("Active Ethernet Device:"); - appendLog(" Interface: " + Nmcli.activeEthernet.interface); - appendLog(" State: " + Nmcli.activeEthernet.state); - appendLog(" Connection: " + (Nmcli.activeEthernet.connection || "None")); - } else { - appendLog("No active ethernet device"); - } - } - } - } - } - } - - // Debug Output Section - StyledRect { - Layout.fillWidth: true - Layout.preferredHeight: 300 - Layout.minimumHeight: 200 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - - StyledText { - text: qsTr("Debug Output") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - TextButton { - text: qsTr("Copy") - onClicked: { - debugOutput.selectAll(); - debugOutput.copy(); - debugOutput.deselect(); - appendLog("Output copied to clipboard"); - } - } - } - - StyledFlickable { - id: debugOutputFlickable - - Layout.fillWidth: true - Layout.fillHeight: true - flickableDirection: Flickable.VerticalFlick - clip: true - - TextEdit { - id: debugOutput - - width: debugOutputFlickable.width - readOnly: true - wrapMode: TextEdit.Wrap - font.family: Appearance.font.family.mono - font.pointSize: Appearance.font.size.smaller - renderType: TextEdit.NativeRendering - textFormat: TextEdit.PlainText - color: "#ffb0ca" // Use primary color - will be set programmatically - - Component.onCompleted: { - color = Colours.palette.m3primary; - appendLog("Debug panel initialized"); - } - - onTextChanged: { - // Ensure color stays set when text changes - color = Colours.palette.m3primary; - // Update content height - debugOutputFlickable.contentHeight = Math.max(implicitHeight, debugOutputFlickable.height); - // Auto-scroll to bottom - Qt.callLater(() => { - if (debugOutputFlickable.contentHeight > debugOutputFlickable.height) { - debugOutputFlickable.contentY = debugOutputFlickable.contentHeight - debugOutputFlickable.height; - } - }); - } - } - } - - StyledScrollBar { - flickable: debugOutputFlickable - policy: ScrollBar.AlwaysOn - } - } - } - } - } - - function appendLog(message: string): void { - const timestamp = new Date().toLocaleTimeString(); - debugOutput.text += `[${timestamp}] ${message}\n`; - } - - function log(message: string): void { - appendLog(message); - } - - Component.onCompleted: { - // Set up debug logger for Nmcli service - Nmcli.setDebugLogger((msg) => { - appendLog("[Nmcli] " + msg); - }); - } -} - diff --git a/modules/controlcenter/dev/DevNavRail.qml b/modules/controlcenter/dev/DevNavRail.qml deleted file mode 100644 index d2f2d57..0000000 --- a/modules/controlcenter/dev/DevNavRail.qml +++ /dev/null @@ -1,194 +0,0 @@ -pragma ComponentBehavior: Bound - -import "." -import ".." -import qs.components -import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts - -Item { - id: root - - required property ShellScreen screen - required property DevSession session - - implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4 - implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 - - ColumnLayout { - id: layout - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.larger * 2 - spacing: Appearance.spacing.normal - - states: State { - name: "expanded" - when: root.session.navExpanded - - PropertyChanges { - layout.spacing: Appearance.spacing.small - menuIcon.opacity: 0 - menuIconExpanded.opacity: 1 - menuIcon.rotation: 180 - menuIconExpanded.rotation: 0 - } - } - - transitions: Transition { - Anim { - properties: "spacing,opacity,rotation" - } - } - - Item { - id: menuBtn - - Layout.topMargin: Appearance.spacing.large - implicitWidth: menuIcon.implicitWidth + menuIcon.anchors.leftMargin * 2 - implicitHeight: menuIcon.implicitHeight + Appearance.padding.normal * 2 - - StateLayer { - radius: Appearance.rounding.small - - function onClicked(): void { - root.session.navExpanded = !root.session.navExpanded; - } - } - - MaterialIcon { - id: menuIcon - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.large - - text: "menu" - font.pointSize: Appearance.font.size.large - } - - MaterialIcon { - id: menuIconExpanded - - anchors.fill: menuIcon - text: "menu_open" - font.pointSize: menuIcon.font.pointSize - opacity: 0 - rotation: -180 - } - } - - NavItem { - Layout.topMargin: Appearance.spacing.large * 2 - icon: "wifi" - label: "wireless" - } - - NavItem { - icon: "bug_report" - label: "debug" - } - } - - component NavItem: Item { - id: item - - required property string icon - required property string label - readonly property bool active: root.session.active === label - - implicitWidth: background.implicitWidth - implicitHeight: background.implicitHeight + smallLabel.implicitHeight + smallLabel.anchors.topMargin - - states: State { - name: "expanded" - when: root.session.navExpanded - - PropertyChanges { - expandedLabel.opacity: 1 - smallLabel.opacity: 0 - background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth - background.implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 - item.implicitHeight: background.implicitHeight - } - } - - transitions: Transition { - Anim { - property: "opacity" - duration: Appearance.anim.durations.small - } - - Anim { - properties: "implicitWidth,implicitHeight" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - StyledRect { - id: background - - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3secondaryContainer, item.active ? 1 : 0) - - implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 - implicitHeight: icon.implicitHeight + Appearance.padding.small - - StateLayer { - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - - function onClicked(): void { - root.session.active = item.label; - } - } - - MaterialIcon { - id: icon - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.large - - text: item.icon - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.large - fill: item.active ? 1 : 0 - - Behavior on fill { - Anim {} - } - } - - StyledText { - id: expandedLabel - - anchors.left: icon.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.spacing.normal - - opacity: 0 - text: item.label - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.capitalization: Font.Capitalize - } - - StyledText { - id: smallLabel - - anchors.horizontalCenter: icon.horizontalCenter - anchors.top: icon.bottom - anchors.topMargin: Appearance.spacing.small / 2 - - text: item.label - font.pointSize: Appearance.font.size.small - font.capitalization: Font.Capitalize - } - } - } -} - diff --git a/modules/controlcenter/dev/DevPanes.qml b/modules/controlcenter/dev/DevPanes.qml deleted file mode 100644 index 6b5ce06..0000000 --- a/modules/controlcenter/dev/DevPanes.qml +++ /dev/null @@ -1,70 +0,0 @@ -pragma ComponentBehavior: Bound - -import "." -import ".." -import qs.components -import qs.services -import qs.config -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts - -ClippingRectangle { - id: root - - required property DevSession session - - color: "transparent" - - ColumnLayout { - id: layout - - spacing: 0 - y: -root.session.activeIndex * root.height - - Pane { - index: 0 - sourceComponent: DevWirelessPane { - session: root.session - } - } - - Pane { - index: 1 - sourceComponent: DevDebugPane { - session: root.session - } - } - - Behavior on y { - Anim {} - } - } - - component Pane: Item { - id: pane - - required property int index - property alias sourceComponent: loader.sourceComponent - - implicitWidth: root.width - implicitHeight: root.height - - Loader { - id: loader - - anchors.fill: parent - clip: true - asynchronous: true - active: { - if (root.session.activeIndex === pane.index) - return true; - - const ly = -layout.y; - const ty = pane.index * root.height; - return ly + root.height > ty && ly < ty + root.height; - } - } - } -} - diff --git a/modules/controlcenter/dev/DevSession.qml b/modules/controlcenter/dev/DevSession.qml deleted file mode 100644 index d911386..0000000 --- a/modules/controlcenter/dev/DevSession.qml +++ /dev/null @@ -1,23 +0,0 @@ -import QtQuick - -QtObject { - readonly property list panes: ["wireless", "debug"] - - required property var root - property bool floating: false - property string active: panes[0] - property int activeIndex: 0 - property bool navExpanded: false - - component Network: QtObject { - property var active - property bool showPasswordDialog: false - property var pendingNetwork - } - - readonly property Network network: Network {} - - onActiveChanged: activeIndex = panes.indexOf(active) - onActiveIndexChanged: active = panes[activeIndex] -} - diff --git a/modules/controlcenter/dev/DevWindowFactory.qml b/modules/controlcenter/dev/DevWindowFactory.qml deleted file mode 100644 index 5682588..0000000 --- a/modules/controlcenter/dev/DevWindowFactory.qml +++ /dev/null @@ -1,62 +0,0 @@ -pragma Singleton - -import "." -import qs.components -import qs.services -import Quickshell -import QtQuick - -Singleton { - id: root - - function create(parent: Item, props: var): void { - devControlCenter.createObject(parent ?? dummy, props); - } - - QtObject { - id: dummy - } - - Component { - id: devControlCenter - - FloatingWindow { - id: win - - property alias active: cc.active - property alias navExpanded: cc.navExpanded - - color: Colours.tPalette.m3surface - - onVisibleChanged: { - if (!visible) - destroy(); - } - - minimumSize.width: 1000 - minimumSize.height: 600 - - implicitWidth: cc.implicitWidth - implicitHeight: cc.implicitHeight - - title: qsTr("Dev Panel - Wireless") - - DevControlCenter { - id: cc - - anchors.fill: parent - screen: win.screen - floating: true - - function close(): void { - win.destroy(); - } - } - - Behavior on color { - CAnim {} - } - } - } -} - diff --git a/modules/controlcenter/dev/DevWindowTitle.qml b/modules/controlcenter/dev/DevWindowTitle.qml deleted file mode 100644 index 9395532..0000000 --- a/modules/controlcenter/dev/DevWindowTitle.qml +++ /dev/null @@ -1,53 +0,0 @@ -import "." -import qs.components -import qs.services -import qs.config -import Quickshell -import QtQuick - -StyledRect { - id: root - - required property ShellScreen screen - required property DevSession session - - implicitHeight: text.implicitHeight + Appearance.padding.normal - color: Colours.tPalette.m3surfaceContainer - - StyledText { - id: text - - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - - text: qsTr("Dev Panel - %1").arg(root.session.active.slice(0, 1).toUpperCase() + root.session.active.slice(1)) - font.capitalization: Font.Capitalize - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - Item { - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Appearance.padding.normal - - implicitWidth: implicitHeight - implicitHeight: closeIcon.implicitHeight + Appearance.padding.small - - StateLayer { - radius: Appearance.rounding.full - - function onClicked(): void { - QsWindow.window.destroy(); - } - } - - MaterialIcon { - id: closeIcon - - anchors.centerIn: parent - text: "close" - } - } -} - diff --git a/modules/controlcenter/dev/DevWirelessPane.qml b/modules/controlcenter/dev/DevWirelessPane.qml deleted file mode 100644 index feb0ce7..0000000 --- a/modules/controlcenter/dev/DevWirelessPane.qml +++ /dev/null @@ -1,68 +0,0 @@ -pragma ComponentBehavior: Bound - -import "." -import ".." -import qs.components -import qs.components.effects -import qs.components.containers -import qs.config -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts - -RowLayout { - id: root - - required property DevSession session - - anchors.fill: parent - - spacing: 0 - - Item { - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - // Blank placeholder for wireless list - Item { - 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 - } - - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: rightBorder.innerRadius - color: "transparent" - - // Blank placeholder for settings/details area - Item { - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - } - } - - InnerBorder { - id: rightBorder - - leftThickness: Appearance.padding.normal / 2 - } - } -} - diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index ccf1c7d..d3ad51e 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -3,7 +3,6 @@ import qs.components.controls import qs.services import qs.config import qs.modules.controlcenter -import "../../controlcenter/dev" import Quickshell import Quickshell.Bluetooth import QtQuick @@ -94,17 +93,6 @@ StyledRect { onClicked: VPN.toggle() } - Toggle { - icon: "bug_report" - inactiveOnColour: Colours.palette.m3onSurfaceVariant - toggle: false - onClicked: { - root.visibilities.utilities = false; - DevWindowFactory.create(null, { - screen: QsWindow.window?.screen ?? null - }); - } - } } } -- cgit v1.2.3-freya From 6eebd15c1eef2fa9dafccdd4036ad23800e1154c Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 21:38:46 -0500 Subject: tray: consolidating ethernet into network --- modules/bar/components/StatusIcons.qml | 6 +- modules/bar/popouts/Content.qml | 5 - modules/bar/popouts/Ethernet.qml | 179 --------------------------------- modules/bar/popouts/Network.qml | 110 ++++++++++++++++++++ 4 files changed, 113 insertions(+), 187 deletions(-) delete mode 100644 modules/bar/popouts/Ethernet.qml diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 6113539..1afa616 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -147,15 +147,15 @@ StyledRect { sourceComponent: MaterialIcon { animate: true - text: Network.active ? Icons.getNetworkIcon(Network.active.strength ?? 0) : "wifi_off" + text: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off" color: root.colour } } // Ethernet icon WrappedLoader { - name: "ethernet" - active: Config.bar.status.showEthernet && Network.activeEthernet + name: "network" + active: Config.bar.status.showEthernet && Nmcli.activeEthernet sourceComponent: MaterialIcon { animate: true diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index f8ef549..e3f569d 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -36,11 +36,6 @@ Item { sourceComponent: Network {} } - Popout { - name: "ethernet" - sourceComponent: Ethernet {} - } - Popout { name: "bluetooth" sourceComponent: Bluetooth { diff --git a/modules/bar/popouts/Ethernet.qml b/modules/bar/popouts/Ethernet.qml deleted file mode 100644 index b4a753c..0000000 --- a/modules/bar/popouts/Ethernet.qml +++ /dev/null @@ -1,179 +0,0 @@ -pragma ComponentBehavior: Bound - -import qs.components -import qs.components.controls -import qs.services -import qs.config -import qs.utils -import Quickshell -import QtQuick -import QtQuick.Layouts - -ColumnLayout { - id: root - - spacing: Appearance.spacing.small - width: Config.bar.sizes.networkWidth - - StyledText { - Layout.topMargin: Appearance.padding.normal - Layout.rightMargin: Appearance.padding.small - text: qsTr("Ethernet") - font.weight: 500 - } - - StyledText { - Layout.topMargin: Appearance.spacing.small - Layout.rightMargin: Appearance.padding.small - text: qsTr("%1 devices available").arg(Network.ethernetDeviceCount || Network.ethernetDevices.length) - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small - } - - Repeater { - model: ScriptModel { - values: [...Network.ethernetDevices].sort((a, b) => { - if (a.connected !== b.connected) - return b.connected - a.connected; - return (a.interface || "").localeCompare(b.interface || ""); - }).slice(0, 8) - } - - RowLayout { - id: ethernetItem - - required property var modelData - readonly property bool loading: false - - Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - spacing: Appearance.spacing.small - - opacity: 0 - scale: 0.7 - - Component.onCompleted: { - opacity = 1; - scale = 1; - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim {} - } - - MaterialIcon { - text: "cable" - color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - } - - StyledText { - Layout.leftMargin: Appearance.spacing.small / 2 - Layout.rightMargin: Appearance.spacing.small / 2 - Layout.fillWidth: true - text: ethernetItem.modelData.interface || qsTr("Unknown") - elide: Text.ElideRight - font.weight: ethernetItem.modelData.connected ? 500 : 400 - color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface - } - - StyledRect { - id: connectBtn - - implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.small - - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0) - - CircularIndicator { - anchors.fill: parent - running: ethernetItem.loading - } - - StateLayer { - color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: ethernetItem.loading - - function onClicked(): void { - if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { - Network.disconnectEthernet(ethernetItem.modelData.connection); - } else { - Network.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || ""); - } - } - } - - MaterialIcon { - id: connectIcon - - anchors.centerIn: parent - animate: true - text: ethernetItem.modelData.connected ? "link_off" : "link" - color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - - opacity: ethernetItem.loading ? 0 : 1 - - Behavior on opacity { - Anim {} - } - } - } - } - } - - StyledRect { - Layout.topMargin: Appearance.spacing.small - Layout.fillWidth: true - implicitHeight: refreshBtn.implicitHeight + Appearance.padding.small * 2 - - radius: Appearance.rounding.full - color: Colours.palette.m3primaryContainer - - StateLayer { - color: Colours.palette.m3onPrimaryContainer - disabled: Network.ethernetProcessRunning - - function onClicked(): void { - Network.getEthernetDevices(); - } - } - - RowLayout { - id: refreshBtn - - anchors.centerIn: parent - spacing: Appearance.spacing.small - opacity: Network.ethernetProcessRunning ? 0 : 1 - - MaterialIcon { - id: refreshIcon - - animate: true - text: "refresh" - color: Colours.palette.m3onPrimaryContainer - } - - StyledText { - text: qsTr("Refresh devices") - color: Colours.palette.m3onPrimaryContainer - } - - Behavior on opacity { - Anim {} - } - } - - CircularIndicator { - anchors.centerIn: parent - strokeWidth: Appearance.padding.small / 2 - bgColour: "transparent" - implicitHeight: parent.implicitHeight - Appearance.padding.smaller * 2 - running: Network.ethernetProcessRunning - } - } -} - diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index cb012bf..d185887 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -192,6 +192,116 @@ ColumnLayout { } } + StyledText { + Layout.topMargin: Appearance.spacing.normal + Layout.rightMargin: Appearance.padding.small + text: qsTr("Ethernet") + font.weight: 500 + } + + StyledText { + Layout.topMargin: Appearance.spacing.small + Layout.rightMargin: Appearance.padding.small + text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length) + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + } + + Repeater { + model: ScriptModel { + values: [...Nmcli.ethernetDevices].sort((a, b) => { + if (a.connected !== b.connected) + return b.connected - a.connected; + return (a.interface || "").localeCompare(b.interface || ""); + }).slice(0, 8) + } + + RowLayout { + id: ethernetItem + + required property var modelData + readonly property bool loading: false + + Layout.fillWidth: true + Layout.rightMargin: Appearance.padding.small + spacing: Appearance.spacing.small + + opacity: 0 + scale: 0.7 + + Component.onCompleted: { + opacity = 1; + scale = 1; + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + MaterialIcon { + text: "cable" + color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + StyledText { + Layout.leftMargin: Appearance.spacing.small / 2 + Layout.rightMargin: Appearance.spacing.small / 2 + Layout.fillWidth: true + text: ethernetItem.modelData.interface || qsTr("Unknown") + elide: Text.ElideRight + font.weight: ethernetItem.modelData.connected ? 500 : 400 + color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface + } + + StyledRect { + id: connectBtn + + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.small + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0) + + CircularIndicator { + anchors.fill: parent + running: ethernetItem.loading + } + + StateLayer { + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: ethernetItem.loading + + function onClicked(): void { + if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { + Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); + } else { + Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {}); + } + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + animate: true + text: ethernetItem.modelData.connected ? "link_off" : "link" + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + + opacity: ethernetItem.loading ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + } + } + } + Connections { target: Nmcli -- cgit v1.2.3-freya From 8d8afba79a58f917906c2457708950d050fa30e7 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 21:40:50 -0500 Subject: tray: removed ethernet config options (network covers it) --- README.md | 1 - config/BarConfig.qml | 1 - modules/bar/components/StatusIcons.qml | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 112a174..a2322eb 100644 --- a/README.md +++ b/README.md @@ -372,7 +372,6 @@ default, you must create it manually. "showAudio": false, "showBattery": true, "showBluetooth": true, - "showEthernet": true, "showKbLayout": false, "showMicrophone": false, "showNetwork": true, diff --git a/config/BarConfig.qml b/config/BarConfig.qml index b81d7e9..34819b1 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -90,7 +90,6 @@ JsonObject { property bool showMicrophone: false property bool showKbLayout: false property bool showNetwork: true - property bool showEthernet: true property bool showBluetooth: true property bool showBattery: true property bool showLockStatus: true diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 1afa616..80e8fe0 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -155,7 +155,7 @@ StyledRect { // Ethernet icon WrappedLoader { name: "network" - active: Config.bar.status.showEthernet && Nmcli.activeEthernet + active: Config.bar.status.showNetwork && Nmcli.activeEthernet sourceComponent: MaterialIcon { animate: true -- cgit v1.2.3-freya From 7dcfed3f0e65a86d0c7a246cea6366dbbd14b067 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 21:52:35 -0500 Subject: tray: wireless, ethernet, bluetooth cleanup --- modules/bar/components/StatusIcons.qml | 2 +- modules/bar/popouts/Bluetooth.qml | 2 +- modules/bar/popouts/Content.qml | 11 ++++++++++- modules/bar/popouts/Network.qml | 33 +++++++++++++++++++++++++++------ 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 80e8fe0..2f52596 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -154,7 +154,7 @@ StyledRect { // Ethernet icon WrappedLoader { - name: "network" + name: "ethernet" active: Config.bar.status.showNetwork && Nmcli.activeEthernet sourceComponent: MaterialIcon { diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index c1f2029..1415f89 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -21,7 +21,7 @@ ColumnLayout { StyledText { Layout.topMargin: Appearance.padding.normal Layout.rightMargin: Appearance.padding.small - text: qsTr("Bluetooth %1").arg(BluetoothAdapterState.toString(Bluetooth.defaultAdapter?.state).toLowerCase()) + text: qsTr("Bluetooth") font.weight: 500 } diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index e3f569d..661a41c 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -33,7 +33,16 @@ Item { Popout { name: "network" - sourceComponent: Network {} + sourceComponent: Network { + view: "wireless" + } + } + + Popout { + name: "ethernet" + sourceComponent: Network { + view: "ethernet" + } } Popout { diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index d185887..b807ce4 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -13,25 +13,33 @@ ColumnLayout { id: root property string connectingToSsid: "" + property string view: "wireless" // "wireless" or "ethernet" spacing: Appearance.spacing.small width: Config.bar.sizes.networkWidth + // Wireless section StyledText { - Layout.topMargin: Appearance.padding.normal + visible: root.view === "wireless" + Layout.preferredHeight: visible ? implicitHeight : 0 + Layout.topMargin: visible ? Appearance.padding.normal : 0 Layout.rightMargin: Appearance.padding.small - text: qsTr("Wifi %1").arg(Nmcli.wifiEnabled ? "enabled" : "disabled") + text: qsTr("Wireless") font.weight: 500 } Toggle { + visible: root.view === "wireless" + Layout.preferredHeight: visible ? implicitHeight : 0 label: qsTr("Enabled") checked: Nmcli.wifiEnabled toggle.onToggled: Nmcli.enableWifi(checked) } StyledText { - Layout.topMargin: Appearance.spacing.small + visible: root.view === "wireless" + Layout.preferredHeight: visible ? implicitHeight : 0 + Layout.topMargin: visible ? Appearance.spacing.small : 0 Layout.rightMargin: Appearance.padding.small text: qsTr("%1 networks available").arg(Nmcli.networks.length) color: Colours.palette.m3onSurfaceVariant @@ -39,6 +47,7 @@ ColumnLayout { } Repeater { + visible: root.view === "wireless" model: ScriptModel { values: [...Nmcli.networks].sort((a, b) => { if (a.active !== b.active) @@ -54,6 +63,8 @@ ColumnLayout { readonly property bool isConnecting: root.connectingToSsid === modelData.ssid readonly property bool loading: networkItem.isConnecting + visible: root.view === "wireless" + Layout.preferredHeight: visible ? implicitHeight : 0 Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small spacing: Appearance.spacing.small @@ -142,7 +153,9 @@ ColumnLayout { } StyledRect { - Layout.topMargin: Appearance.spacing.small + visible: root.view === "wireless" + Layout.preferredHeight: visible ? implicitHeight : 0 + Layout.topMargin: visible ? Appearance.spacing.small : 0 Layout.fillWidth: true implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small * 2 @@ -192,15 +205,20 @@ ColumnLayout { } } + // Ethernet section StyledText { - Layout.topMargin: Appearance.spacing.normal + visible: root.view === "ethernet" + Layout.preferredHeight: visible ? implicitHeight : 0 + Layout.topMargin: visible ? Appearance.padding.normal : 0 Layout.rightMargin: Appearance.padding.small text: qsTr("Ethernet") font.weight: 500 } StyledText { - Layout.topMargin: Appearance.spacing.small + visible: root.view === "ethernet" + Layout.preferredHeight: visible ? implicitHeight : 0 + Layout.topMargin: visible ? Appearance.spacing.small : 0 Layout.rightMargin: Appearance.padding.small text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length) color: Colours.palette.m3onSurfaceVariant @@ -208,6 +226,7 @@ ColumnLayout { } Repeater { + visible: root.view === "ethernet" model: ScriptModel { values: [...Nmcli.ethernetDevices].sort((a, b) => { if (a.connected !== b.connected) @@ -222,6 +241,8 @@ ColumnLayout { required property var modelData readonly property bool loading: false + visible: root.view === "ethernet" + Layout.preferredHeight: visible ? implicitHeight : 0 Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small spacing: Appearance.spacing.small -- cgit v1.2.3-freya From 78d032e9462b6678691747bfacc21032ee2e5685 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 22:16:47 -0500 Subject: controlcenter: collapsiblegroups animation and styling tweaked --- components/StateLayer.qml | 3 +- components/controls/CollapsibleSection.qml | 111 +++++++++++++-------- .../controlcenter/appearance/AppearancePane.qml | 20 ---- 3 files changed, 70 insertions(+), 64 deletions(-) diff --git a/components/StateLayer.qml b/components/StateLayer.qml index d86e782..a20e266 100644 --- a/components/StateLayer.qml +++ b/components/StateLayer.qml @@ -6,6 +6,7 @@ MouseArea { id: root property bool disabled + property bool showHoverBackground: true property color color: Colours.palette.m3onSurface property real radius: parent?.radius ?? 0 property alias rect: hoverLayer @@ -75,7 +76,7 @@ MouseArea { anchors.fill: parent - color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.1 : root.containsMouse ? 0.08 : 0) + color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.12 : (root.showHoverBackground && root.containsMouse) ? 0.08 : 0) radius: root.radius StyledRect { diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index cb6e62a..a22ad99 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -15,71 +15,96 @@ ColumnLayout { signal toggleRequested - spacing: Appearance.spacing.small / 2 + spacing: 0 Layout.fillWidth: true Item { id: sectionHeaderItem Layout.fillWidth: true - Layout.preferredHeight: sectionHeader.implicitHeight + Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48) - ColumnLayout { - id: sectionHeader + RowLayout { + id: titleRow anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + spacing: Appearance.spacing.normal - StyledText { - Layout.topMargin: Appearance.spacing.large - text: root.title - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + StyledText { + text: root.title + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - MaterialIcon { - text: "expand_more" - rotation: root.expanded ? 180 : 0 - color: Colours.palette.m3onSurface - Behavior on rotation { - Anim {} + MaterialIcon { + text: "expand_more" + rotation: root.expanded ? 180 : 0 + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + Behavior on rotation { + Anim { + duration: Appearance.anim.durations.short + easing.bezierCurve: Appearance.anim.curves.standard } } } + } - StateLayer { - anchors.fill: parent - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal - function onClicked(): void { - root.toggleRequested(); - root.expanded = !root.expanded; - } - } - - StyledText { - visible: root.expanded && root.description !== "" - text: root.description - color: Colours.palette.m3outline - Layout.fillWidth: true + StateLayer { + anchors.fill: parent + color: Colours.palette.m3onSurface + radius: Appearance.rounding.normal + showHoverBackground: false + function onClicked(): void { + root.toggleRequested(); + root.expanded = !root.expanded; } } } + StyledText { + visible: root.expanded && root.description !== "" + Layout.fillWidth: true + Layout.leftMargin: Appearance.padding.normal + Layout.rightMargin: Appearance.padding.normal + Layout.topMargin: Appearance.spacing.smaller + Layout.bottomMargin: Appearance.spacing.small + text: root.description + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + } + default property alias content: contentColumn.data - ColumnLayout { - id: contentColumn + Item { + id: contentWrapper Layout.fillWidth: true - visible: root.expanded - spacing: Appearance.spacing.small / 2 + Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0 + clip: true + + Behavior on Layout.preferredHeight { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + ColumnLayout { + id: contentColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + anchors.topMargin: Appearance.spacing.small + anchors.bottomMargin: Appearance.spacing.small + spacing: Appearance.spacing.small + } } } diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index fc338f9..e4d5892 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -284,10 +284,6 @@ RowLayout { Layout.fillWidth: true implicitHeight: colorVariantSection.expanded ? Math.min(400, M3Variants.list.length * 60) : 0 - Behavior on implicitHeight { - Anim {} - } - model: M3Variants.list spacing: Appearance.spacing.small / 2 clip: true @@ -379,10 +375,6 @@ RowLayout { Layout.fillWidth: true implicitHeight: colorSchemeSection.expanded ? Math.min(400, Schemes.list.length * 80) : 0 - Behavior on implicitHeight { - Anim {} - } - model: Schemes.list spacing: Appearance.spacing.small / 2 clip: true @@ -556,10 +548,6 @@ RowLayout { Layout.fillWidth: true implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - Behavior on implicitHeight { - Anim {} - } - model: Qt.fontFamilies() spacing: Appearance.spacing.small / 2 clip: true @@ -633,10 +621,6 @@ RowLayout { Layout.fillWidth: true implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - Behavior on implicitHeight { - Anim {} - } - model: Qt.fontFamilies() spacing: Appearance.spacing.small / 2 clip: true @@ -710,10 +694,6 @@ RowLayout { Layout.fillWidth: true implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - Behavior on implicitHeight { - Anim {} - } - model: Qt.fontFamilies() spacing: Appearance.spacing.small / 2 clip: true -- cgit v1.2.3-freya From 97e1bde96b698cb85cd663c8d4343a6b40da34a3 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 22:21:29 -0500 Subject: controlcenter: appearance pane tweaks --- .../controlcenter/appearance/AppearancePane.qml | 35 +++++++--------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index e4d5892..efd67e9 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -997,47 +997,34 @@ RowLayout { font.bold: true } - StyledText { - Layout.topMargin: Appearance.spacing.large - Layout.alignment: Qt.AlignHCenter - text: qsTr("Theme mode") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: Colours.currentLight ? qsTr("Light mode") : qsTr("Dark mode") - color: Colours.palette.m3outline - } - StyledText { Layout.topMargin: Appearance.spacing.large Layout.alignment: Qt.AlignHCenter text: qsTr("Wallpaper") - font.pointSize: Appearance.font.size.larger - font.weight: 500 + font.pointSize: Appearance.font.size.extraLarge + font.weight: 600 } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Select a wallpaper") - color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant } GridLayout { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Appearance.spacing.large Layout.alignment: Qt.AlignHCenter - columns: Math.max(1, Math.floor(parent.width / 200)) + columns: Math.max(2, Math.floor(parent.width / 200)) rowSpacing: Appearance.spacing.normal columnSpacing: Appearance.spacing.normal // Center the grid content Layout.maximumWidth: { const cols = columns; - const itemWidth = 180; + const itemWidth = 200; const spacing = columnSpacing; return cols * itemWidth + (cols - 1) * spacing; } @@ -1048,10 +1035,10 @@ RowLayout { delegate: Item { required property var modelData - Layout.preferredWidth: 180 - Layout.preferredHeight: 120 - Layout.minimumWidth: 180 - Layout.minimumHeight: 120 + Layout.preferredWidth: 200 + Layout.preferredHeight: 140 + Layout.minimumWidth: 200 + Layout.minimumHeight: 140 readonly property bool isCurrent: modelData.path === Wallpapers.actualCurrent readonly property real imageWidth: Math.max(1, width) -- cgit v1.2.3-freya From 144d04b08b132c9ee662fbde79be7f31e18b3d5b Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 22:42:46 -0500 Subject: controlcenter: password dialog matches lockscreen --- components/controls/StyledTextField.qml | 2 +- .../network/WirelessPasswordDialog.qml | 216 +++++++++++++++++---- 2 files changed, 180 insertions(+), 38 deletions(-) diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml index 4db87e9..60bcff2 100644 --- a/components/controls/StyledTextField.qml +++ b/components/controls/StyledTextField.qml @@ -13,7 +13,7 @@ TextField { placeholderTextColor: Colours.palette.m3outline font.family: Appearance.font.family.sans font.pointSize: Appearance.font.size.smaller - renderType: TextField.NativeRendering + renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering cursorVisible: !readOnly background: null diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index e416016..1ea39c9 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -8,6 +8,7 @@ import qs.components.effects import qs.components.containers import qs.services import qs.config +import Quickshell import QtQuick import QtQuick.Layouts @@ -27,7 +28,8 @@ Item { return null; } - visible: session.network.showPasswordDialog + property bool isClosing: false + visible: session.network.showPasswordDialog && !isClosing enabled: visible focus: visible @@ -38,10 +40,14 @@ Item { Rectangle { anchors.fill: parent color: Qt.rgba(0, 0, 0, 0.5) - opacity: root.visible ? 1 : 0 + opacity: root.visible && !root.isClosing ? 1 : 0 Behavior on opacity { - NumberAnimation { duration: 200 } + Anim { + property: "opacity" + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardDecel + } } MouseArea { @@ -60,15 +66,23 @@ Item { radius: Appearance.rounding.normal color: Colours.tPalette.m3surface - opacity: root.visible ? 1 : 0 - scale: root.visible ? 1 : 0.9 + opacity: root.visible && !root.isClosing ? 1 : 0 + scale: root.visible && !root.isClosing ? 1 : 0.9 Behavior on opacity { - NumberAnimation { duration: 200 } + Anim { + property: "opacity" + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardDecel + } } Behavior on scale { - NumberAnimation { duration: 200 } + Anim { + property: "scale" + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardDecel + } } Keys.onEscapePressed: closeDialog(); @@ -123,53 +137,151 @@ Item { } Item { + id: passwordContainer Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true - implicitHeight: passwordField.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + + focus: true + Keys.onPressed: event => { + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { + if (connectButton.enabled) { + connectButton.clicked(); + } + } else if (event.key === Qt.Key_Backspace) { + if (event.modifiers & Qt.ControlModifier) { + passwordBuffer = ""; + } else { + passwordBuffer = passwordBuffer.slice(0, -1); + } + } else if (event.text && event.text.length > 0) { + passwordBuffer += event.text; + } + } + + property string passwordBuffer: "" + + Connections { + target: root + function onVisibleChanged(): void { + if (root.visible) { + passwordContainer.forceActiveFocus(); + passwordContainer.passwordBuffer = ""; + } + } + } StyledRect { anchors.fill: parent radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer - border.width: passwordField.activeFocus ? 2 : 1 - border.color: passwordField.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline + border.width: passwordContainer.activeFocus ? 2 : 1 + border.color: passwordContainer.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline Behavior on border.color { CAnim {} } } - StyledTextField { - id: passwordField + StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor + + function onClicked(): void { + passwordContainer.forceActiveFocus(); + } + } + + StyledText { + id: placeholder + anchors.centerIn: parent + text: qsTr("Password") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + font.family: Appearance.font.family.mono + opacity: passwordContainer.passwordBuffer ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + + ListView { + id: charList + + readonly property int fullWidth: count * (implicitHeight + spacing) - spacing + + anchors.centerIn: parent + implicitWidth: fullWidth + implicitHeight: Appearance.font.size.normal + + orientation: Qt.Horizontal + spacing: Appearance.spacing.small / 2 + interactive: false + + model: ScriptModel { + values: passwordContainer.passwordBuffer.split("") + } + + delegate: StyledRect { + id: ch - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + implicitWidth: implicitHeight + implicitHeight: charList.implicitHeight - echoMode: TextField.Password - placeholderText: qsTr("Password") + color: Colours.palette.m3onSurface + radius: Appearance.rounding.small / 2 + + opacity: 0 + scale: 0 + Component.onCompleted: { + opacity = 1; + scale = 1; + } + ListView.onRemove: removeAnim.start() - Connections { - target: root - function onVisibleChanged(): void { - if (root.visible) { - passwordField.forceActiveFocus(); - passwordField.text = ""; + SequentialAnimation { + id: removeAnim + + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: true + } + ParallelAnimation { + Anim { + target: ch + property: "opacity" + to: 0 + } + Anim { + target: ch + property: "scale" + to: 0.5 + } + } + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: false } } - } - Keys.onReturnPressed: { - if (connectButton.enabled) { - connectButton.clicked(); + Behavior on opacity { + Anim {} } - } - Keys.onEnterPressed: { - if (connectButton.enabled) { - connectButton.clicked(); + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } } } + + Behavior on implicitWidth { + Anim {} + } } } @@ -196,7 +308,7 @@ Item { color: Colours.palette.m3primary onColor: Colours.palette.m3onPrimary text: qsTr("Connect") - enabled: passwordField.text.length > 0 && !connecting + enabled: passwordContainer.passwordBuffer.length > 0 && !connecting property bool connecting: false @@ -205,7 +317,7 @@ Item { return; } - const password = passwordField.text; + const password = passwordContainer.passwordBuffer; if (!password || password.length === 0) { return; } @@ -220,7 +332,19 @@ Item { root.network.ssid, password, root.network.bssid || "", - null + (result) => { + if (result && result.success) { + // Connection successful, monitor will handle the rest + } else if (result && result.needsPassword) { + // Shouldn't happen since we provided password + connectionMonitor.stop(); + connecting = false; + enabled = true; + text = qsTr("Connect"); + } else { + // Connection failed, monitor will handle timeout + } + } ); // Start monitoring connection @@ -295,13 +419,31 @@ Item { checkConnectionStatus(); } } + function onConnectionFailed(ssid: string) { + if (root.visible && root.network && root.network.ssid === ssid && connectButton.connecting) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + } + } } function closeDialog(): void { - session.network.showPasswordDialog = false; - passwordField.text = ""; + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; connectButton.connecting = false; connectButton.text = qsTr("Connect"); connectionMonitor.stop(); + + // Wait for fade-out animation to complete before actually hiding + Qt.callLater(() => { + session.network.showPasswordDialog = false; + isClosing = false; + }, Appearance.anim.durations.normal); } } -- cgit v1.2.3-freya From a004902bc6ff931342d7bc18ef4b448205f833d2 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 08:17:21 -0500 Subject: controlcenter: correcting cancel/connect sizing issues --- modules/controlcenter/network/WirelessPasswordDialog.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 1ea39c9..d1ce066 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -294,6 +294,7 @@ Item { id: cancelButton Layout.fillWidth: true + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 color: Colours.palette.m3secondaryContainer onColor: Colours.palette.m3onSecondaryContainer text: qsTr("Cancel") @@ -305,6 +306,7 @@ Item { id: connectButton Layout.fillWidth: true + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 color: Colours.palette.m3primary onColor: Colours.palette.m3onPrimary text: qsTr("Connect") -- cgit v1.2.3-freya From d9313ca42503127e6f5faccda7f8d7ad3d03f36d Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 08:19:22 -0500 Subject: controlcenter: correcting forget network button sizing issues --- modules/controlcenter/network/WirelessDetails.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 4f11589..ff86b8a 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -87,6 +87,7 @@ Item { SimpleButton { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 visible: { if (!root.network || !root.network.ssid) { return false; -- cgit v1.2.3-freya From ba5dfbd4c48352856865687fa90aba1b1cdd6fb7 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 09:00:05 -0500 Subject: network: removed all execs from Network.qml, now relies only on Nmcli.qml --- services/Network.qml | 1086 +++++++++----------------------------------------- 1 file changed, 186 insertions(+), 900 deletions(-) diff --git a/services/Network.qml b/services/Network.qml index 2b79f12..fc16915 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -3,6 +3,7 @@ pragma Singleton import Quickshell import Quickshell.Io import QtQuick +import qs.services Singleton { id: root @@ -13,13 +14,24 @@ Singleton { getEthernetDevices(); }); // Load saved connections on startup - listConnectionsProc.running = true; + Nmcli.loadSavedConnections(() => { + root.savedConnections = Nmcli.savedConnections; + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + }); + // Get initial WiFi status + Nmcli.getWifiStatus((enabled) => { + root.wifiEnabled = enabled; + }); + // Sync networks from Nmcli on startup + Qt.callLater(() => { + syncNetworksFromNmcli(); + }, 100); } readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property bool wifiEnabled: true - readonly property bool scanning: rescanProc.running + readonly property bool scanning: Nmcli.scanning property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null @@ -29,334 +41,245 @@ Singleton { property var wirelessDeviceDetails: null function enableWifi(enabled: bool): void { - const cmd = enabled ? "on" : "off"; - enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]); + Nmcli.enableWifi(enabled, (result) => { + if (result.success) { + root.getWifiStatus(); + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + } + }); } function toggleWifi(): void { - const cmd = wifiEnabled ? "off" : "on"; - enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]); + Nmcli.toggleWifi((result) => { + if (result.success) { + root.getWifiStatus(); + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + } + }); } function rescanWifi(): void { - rescanProc.running = true; + Nmcli.rescanWifi(); } property var pendingConnection: null signal connectionFailed(string ssid) function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { - // When password is provided, use BSSID for more reliable connection - // When no password, use SSID (will use saved password if available) - const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; - let cmd = []; - // Set up pending connection tracking if callback provided if (callback) { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; } - - if (password && password.length > 0) { - // When password is provided, try BSSID first if available, otherwise use SSID - if (hasBssid) { - // Use BSSID when password is provided - ensure BSSID is uppercase - const bssidUpper = bssid.toUpperCase(); - - // Check if a connection with this SSID already exists - const existingConnection = root.savedConnections.find(conn => - conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() - ); - - if (existingConnection) { - // Connection already exists - delete it first, then create new one with updated password - deleteConnectionProc.exec(["nmcli", "connection", "delete", existingConnection]); - // Wait a moment for deletion to complete, then create new connection - Qt.callLater(() => { - createConnectionWithPassword(ssid, bssidUpper, password); - }, 300); - return; - } else { - // No existing connection, create new one - createConnectionWithPassword(ssid, bssidUpper, password); - return; - } + + Nmcli.connectToNetwork(ssid, password, bssid, (result) => { + if (result && result.success) { + // Connection successful + if (callback) callback(result); + root.pendingConnection = null; + } else if (result && result.needsPassword) { + // Password needed - callback will handle showing dialog + if (callback) callback(result); } else { - // Fallback to SSID if BSSID not available - use device wifi connect - cmd = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; + // Connection failed + if (result && result.error) { + root.connectionFailed(ssid); + } + if (callback) callback(result); + root.pendingConnection = null; } - } else { - // Try to connect to existing connection first (will use saved password if available) - cmd = ["nmcli", "device", "wifi", "connect", ssid]; - } - - // Set command and start process - connectProc.command = cmd; - - // If we're creating a connection profile, we need to activate it after creation - const isConnectionAdd = cmd.length > 0 && cmd[0] === "nmcli" && cmd[1] === "connection" && cmd[2] === "add"; - - // Wait a moment before starting to ensure command is set - Qt.callLater(() => { - connectProc.running = true; - }); - - // Start connection check timer if we have a callback - if (callback) { - connectionCheckTimer.start(); - } - } - - function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string): void { - // Create connection profile with all required properties for BSSID + password - const cmd = ["nmcli", "connection", "add", - "type", "wifi", - "con-name", ssid, - "ifname", "*", - "ssid", ssid, - "802-11-wireless.bssid", bssidUpper, - "802-11-wireless-security.key-mgmt", "wpa-psk", - "802-11-wireless-security.psk", password]; - - // Set command and start process - connectProc.command = cmd; - - Qt.callLater(() => { - connectProc.running = true; }); } function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { - // For secure networks, try connecting without password first - // If connection succeeds (saved password exists), we're done - // If it fails with password error, callback will be called to show password dialog - if (isSecure) { - const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; - root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; - // Try connecting without password - will use saved password if available - connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]); - // Start timer to check if connection succeeded - connectionCheckTimer.start(); - } else { - connectToNetwork(ssid, "", bssid, null); - } + // Set up pending connection tracking + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; + + Nmcli.connectToNetworkWithPasswordCheck(ssid, isSecure, (result) => { + if (result && result.success) { + // Connection successful + if (callback) callback(result); + root.pendingConnection = null; + } else if (result && result.needsPassword) { + // Password needed - callback will handle showing dialog + if (callback) callback(result); + } else { + // Connection failed + if (result && result.error) { + root.connectionFailed(ssid); + } + if (callback) callback(result); + root.pendingConnection = null; + } + }, bssid); } function disconnectFromNetwork(): void { // Try to disconnect - use connection name if available, otherwise use device - if (active && active.ssid) { - // First try to disconnect by connection name (more reliable) - disconnectByConnectionProc.exec(["nmcli", "connection", "down", active.ssid]); - } else { - // Fallback: disconnect by device - disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); - } + Nmcli.disconnectFromNetwork(); + // Refresh network list after disconnection + Qt.callLater(() => { + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + }, 500); } function forgetNetwork(ssid: string): void { // Delete the connection profile for this network // This will remove the saved password and connection settings - if (ssid && ssid.length > 0) { - deleteConnectionProc.exec(["nmcli", "connection", "delete", ssid]); - // Also refresh network list after deletion - Qt.callLater(() => { - getNetworks.running = true; - }, 500); - } + Nmcli.forgetNetwork(ssid, (result) => { + if (result.success) { + // Refresh network list after deletion + Qt.callLater(() => { + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + }, 500); + } + }); } - function hasConnectionProfile(ssid: string): bool { - // Check if a connection profile exists for this SSID - // This is synchronous check - returns true if connection exists - if (!ssid || ssid.length === 0) { - return false; - } - // Use nmcli to check if connection exists - // We'll use a Process to check, but for now return false - // The actual check will be done asynchronously - return false; - } property list savedConnections: [] property list savedConnectionSsids: [] - property var wifiConnectionQueue: [] - property int currentSsidQueryIndex: 0 - - Process { - id: listConnectionsProc - command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - onExited: { - if (exitCode === 0) { - parseConnectionList(stdout.text); - } - } - stdout: StdioCollector { - onStreamFinished: { - parseConnectionList(text); - } - } - } - - function parseConnectionList(output: string): void { - const lines = output.trim().split("\n").filter(line => line.length > 0); - const wifiConnections = []; - const connections = []; - - // First pass: identify WiFi connections - for (const line of lines) { - const parts = line.split(":"); - if (parts.length >= 2) { - const name = parts[0]; - const type = parts[1]; - connections.push(name); - if (type === "802-11-wireless") { - wifiConnections.push(name); + // Sync saved connections from Nmcli when they're updated + Connections { + target: Nmcli + function onSavedConnectionsChanged() { + root.savedConnections = Nmcli.savedConnections; + } + function onSavedConnectionSsidsChanged() { + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + } + } + + function syncNetworksFromNmcli(): void { + const rNetworks = root.networks; + const nNetworks = Nmcli.networks; + + // Build a map of existing networks by key + const existingMap = new Map(); + for (const rn of rNetworks) { + const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`; + existingMap.set(key, rn); + } + + // Build a map of new networks by key + const newMap = new Map(); + for (const nn of nNetworks) { + const key = `${nn.frequency}:${nn.ssid}:${nn.bssid}`; + newMap.set(key, nn); + } + + // Remove networks that no longer exist + for (const [key, network] of existingMap) { + if (!newMap.has(key)) { + const index = rNetworks.indexOf(network); + if (index >= 0) { + rNetworks.splice(index, 1); + network.destroy(); } } } - - root.savedConnections = connections; - - // Second pass: get SSIDs for WiFi connections - if (wifiConnections.length > 0) { - root.wifiConnectionQueue = wifiConnections; - root.currentSsidQueryIndex = 0; - root.savedConnectionSsids = []; - // Start querying SSIDs one by one - queryNextSsid(); - } else { - root.savedConnectionSsids = []; - root.wifiConnectionQueue = []; - } - } - - Process { - id: getSsidProc - - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - onExited: { - if (exitCode === 0) { - processSsidOutput(stdout.text); + + // Add or update networks from Nmcli + for (const [key, nNetwork] of newMap) { + const existing = existingMap.get(key); + if (existing) { + // Update existing network's lastIpcObject + existing.lastIpcObject = nNetwork.lastIpcObject; } else { - // Move to next connection even if this one failed - queryNextSsid(); - } - } - stdout: StdioCollector { - onStreamFinished: { - processSsidOutput(text); + // Create new AccessPoint from Nmcli's data + rNetworks.push(apComp.createObject(root, { + lastIpcObject: nNetwork.lastIpcObject + })); } } } - function processSsidOutput(output: string): void { - // Parse "802-11-wireless.ssid:SSID_NAME" format - const lines = output.trim().split("\n"); - for (const line of lines) { - if (line.startsWith("802-11-wireless.ssid:")) { - const ssid = line.substring("802-11-wireless.ssid:".length).trim(); - if (ssid && ssid.length > 0) { - // Add to list if not already present (case-insensitive) - const ssidLower = ssid.toLowerCase(); - if (!root.savedConnectionSsids.some(s => s && s.toLowerCase() === ssidLower)) { - // Create new array to trigger QML property change notification - const newList = root.savedConnectionSsids.slice(); - newList.push(ssid); - root.savedConnectionSsids = newList; - } - } - } - } - - // Query next connection - queryNextSsid(); + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 } - function queryNextSsid(): void { - if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { - const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; - root.currentSsidQueryIndex++; - getSsidProc.command = ["nmcli", "-t", "-f", "802-11-wireless.ssid", "connection", "show", connectionName]; - getSsidProc.running = true; - } else { - // All SSIDs retrieved - root.wifiConnectionQueue = []; - root.currentSsidQueryIndex = 0; - } + Component { + id: apComp + AccessPoint {} } function hasSavedProfile(ssid: string): bool { - if (!ssid || ssid.length === 0) { - return false; - } - const ssidLower = ssid.toLowerCase().trim(); - - // If currently connected to this network, it definitely has a saved profile - if (root.active && root.active.ssid) { - const activeSsidLower = root.active.ssid.toLowerCase().trim(); - if (activeSsidLower === ssidLower) { - return true; - } - } - - // Check if SSID is in saved connections (case-insensitive comparison) - const hasSsid = root.savedConnectionSsids.some(savedSsid => - savedSsid && savedSsid.toLowerCase().trim() === ssidLower - ); - - if (hasSsid) { - return true; - } - - // Fallback: also check if connection name matches SSID (some connections use SSID as name) - const hasConnectionName = root.savedConnections.some(connName => - connName && connName.toLowerCase().trim() === ssidLower - ); - - return hasConnectionName; + // Use Nmcli's hasSavedProfile which has the same logic + return Nmcli.hasSavedProfile(ssid); } function getWifiStatus(): void { - wifiStatusProc.running = true; + Nmcli.getWifiStatus((enabled) => { + root.wifiEnabled = enabled; + }); } function getEthernetDevices(): void { - getEthernetDevicesProc.running = true; + root.ethernetProcessRunning = true; + Nmcli.getEthernetInterfaces((interfaces) => { + root.ethernetDevices = Nmcli.ethernetDevices; + root.ethernetDeviceCount = Nmcli.ethernetDevices.length; + root.ethernetProcessRunning = false; + }); } function connectEthernet(connectionName: string, interfaceName: string): void { - if (connectionName && connectionName.length > 0) { - // Use connection name if available - connectEthernetProc.exec(["nmcli", "connection", "up", connectionName]); - } else if (interfaceName && interfaceName.length > 0) { - // Fallback to device interface if no connection name - connectEthernetProc.exec(["nmcli", "device", "connect", interfaceName]); - } + Nmcli.connectEthernet(connectionName, interfaceName, (result) => { + if (result.success) { + getEthernetDevices(); + // Refresh device details after connection + Qt.callLater(() => { + const activeDevice = root.ethernetDevices.find(function(d) { return d.connected; }); + if (activeDevice && activeDevice.interface) { + updateEthernetDeviceDetails(activeDevice.interface); + } + }, 1000); + } + }); } function disconnectEthernet(connectionName: string): void { - disconnectEthernetProc.exec(["nmcli", "connection", "down", connectionName]); + Nmcli.disconnectEthernet(connectionName, (result) => { + if (result.success) { + getEthernetDevices(); + // Clear device details after disconnection + Qt.callLater(() => { + root.ethernetDeviceDetails = null; + }); + } + }); } function updateEthernetDeviceDetails(interfaceName: string): void { - if (interfaceName && interfaceName.length > 0) { - getEthernetDetailsProc.exec(["nmcli", "device", "show", interfaceName]); - } else { - ethernetDeviceDetails = null; - } + Nmcli.getEthernetDeviceDetails(interfaceName, (details) => { + root.ethernetDeviceDetails = details; + }); } function updateWirelessDeviceDetails(): void { // Find the wireless interface by looking for wifi devices - findWirelessInterfaceProc.exec(["nmcli", "device", "status"]); + // Pass empty string to let Nmcli find the active interface automatically + Nmcli.getWirelessDeviceDetails("", (details) => { + root.wirelessDeviceDetails = details; + }); } function cidrToSubnetMask(cidr: string): string { @@ -382,649 +305,12 @@ Singleton { command: ["nmcli", "m"] stdout: SplitParser { onRead: { - getNetworks.running = true; + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); getEthernetDevices(); } } } - Process { - id: wifiStatusProc - - running: true - command: ["nmcli", "radio", "wifi"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - root.wifiEnabled = text.trim() === "enabled"; - } - } - } - - Process { - id: enableWifiProc - - onExited: { - root.getWifiStatus(); - getNetworks.running = true; - } - } - - Process { - id: rescanProc - - command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"] - onExited: { - getNetworks.running = true; - } - } - - Timer { - id: connectionCheckTimer - interval: 4000 - onTriggered: { - if (root.pendingConnection) { - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - - if (!connected && root.pendingConnection.callback) { - // Connection didn't succeed after multiple checks, show password dialog - const pending = root.pendingConnection; - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - pending.callback(); - } else if (connected) { - // Connection succeeded, clear pending - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } - } - - Timer { - id: immediateCheckTimer - interval: 500 - repeat: true - triggeredOnStart: false - property int checkCount: 0 - - onTriggered: { - if (root.pendingConnection) { - checkCount++; - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - - if (connected) { - // Connection succeeded, stop timers and clear pending - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - root.pendingConnection = null; - } else if (checkCount >= 6) { - // Checked 6 times (3 seconds total), connection likely failed - // Stop immediate check, let the main timer handle it - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } else { - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } - - Process { - id: connectProc - - onExited: { - - // Check if this was a "connection add" command - if so, we need to activate it - const wasConnectionAdd = connectProc.command && connectProc.command.length > 0 - && connectProc.command[0] === "nmcli" - && connectProc.command[1] === "connection" - && connectProc.command[2] === "add"; - - if (wasConnectionAdd && root.pendingConnection) { - const ssid = root.pendingConnection.ssid; - - // Check for duplicate connection warning in stderr text - const stderrText = connectProc.stderr ? connectProc.stderr.text : ""; - const hasDuplicateWarning = stderrText && ( - stderrText.includes("another connection with the name") || - stderrText.includes("Reference the connection by its uuid") - ); - - // Even with duplicate warning (or if connection already exists), we should try to activate it - // Also try if exit code is non-zero but small (might be a warning, not a real error) - if (exitCode === 0 || hasDuplicateWarning || (exitCode > 0 && exitCode < 10)) { - - // Update saved connections list - listConnectionsProc.running = true; - - // Try to activate the connection by SSID (connection name) - connectProc.command = ["nmcli", "connection", "up", ssid]; - Qt.callLater(() => { - connectProc.running = true; - }); - // Don't start timers yet - wait for activation to complete - return; - } else { - // Connection add failed - try using device wifi connect as fallback - // Extract password from the command if available - let password = ""; - if (connectProc.command) { - const pskIndex = connectProc.command.findIndex(arg => arg === "802-11-wireless-security.psk"); - if (pskIndex >= 0 && pskIndex + 1 < connectProc.command.length) { - password = connectProc.command[pskIndex + 1]; - } - } - - if (password && password.length > 0) { - connectProc.command = ["nmcli", "device", "wifi", "connect", ssid, "password", password]; - Qt.callLater(() => { - connectProc.running = true; - }); - return; - } - } - } - - // Refresh network list after connection attempt - getNetworks.running = true; - - // Check if connection succeeded after a short delay (network list needs to update) - if (root.pendingConnection) { - immediateCheckTimer.start(); - } - } - stdout: SplitParser { - onRead: { - getNetworks.running = true; - } - } - stderr: StdioCollector { - onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0) { - // Check for specific errors that indicate password is needed - // Be careful not to match success messages - const needsPassword = (error.includes("Secrets were required") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - (error.includes("password") && !error.includes("Connection activated")) || - (error.includes("Secrets") && !error.includes("Connection activated")) || - (error.includes("802.11") && !error.includes("Connection activated"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); - - if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { - // Connection failed because password is needed - show dialog immediately - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - const pending = root.pendingConnection; - root.pendingConnection = null; - pending.callback(); - } else if (error && error.length > 0 && !error.includes("Connection activated") && !error.includes("successfully")) { - // Emit signal for UI to handle - root.connectionFailed(root.pendingConnection ? root.pendingConnection.ssid : ""); - } - } - } - } - } - - Process { - id: disconnectProc - - onExited: { - // Refresh network list after disconnection - getNetworks.running = true; - } - stdout: SplitParser { - onRead: getNetworks.running = true - } - stderr: StdioCollector { - onStreamFinished: { - } - } - } - - Process { - id: disconnectByConnectionProc - - onExited: { - // Refresh network list after disconnection - getNetworks.running = true; - } - stdout: SplitParser { - onRead: getNetworks.running = true - } - stderr: StdioCollector { - onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) { - // If connection down failed, try device disconnect as fallback - disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]); - } - } - } - } - - Process { - id: deleteConnectionProc - - // Delete connection profile - refresh network list and saved connections after deletion - onExited: { - // Refresh network list and saved connections after deletion - getNetworks.running = true; - listConnectionsProc.running = true; - } - stderr: StdioCollector { - onStreamFinished: { - } - } - } - - Process { - id: getNetworks - - running: true - command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; - const rep = new RegExp("\\\\:", "g"); - const rep2 = new RegExp(PLACEHOLDER, "g"); - - const allNetworks = text.trim().split("\n").map(n => { - const net = n.replace(rep, PLACEHOLDER).split(":"); - return { - active: net[0] === "yes", - strength: parseInt(net[1]), - frequency: parseInt(net[2]), - ssid: net[3]?.replace(rep2, ":") ?? "", - bssid: net[4]?.replace(rep2, ":") ?? "", - security: net[5] ?? "" - }; - }).filter(n => n.ssid && n.ssid.length > 0); - - // Group networks by SSID and prioritize connected ones - const networkMap = new Map(); - for (const network of allNetworks) { - const existing = networkMap.get(network.ssid); - if (!existing) { - networkMap.set(network.ssid, network); - } else { - // Prioritize active/connected networks - if (network.active && !existing.active) { - networkMap.set(network.ssid, network); - } else if (!network.active && !existing.active) { - // If both are inactive, keep the one with better signal - if (network.strength > existing.strength) { - networkMap.set(network.ssid, network); - } - } - // If existing is active and new is not, keep existing - } - } - - const networks = Array.from(networkMap.values()); - - const rNetworks = root.networks; - - const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid)); - for (const network of destroyed) - rNetworks.splice(rNetworks.indexOf(network), 1).forEach(n => n.destroy()); - - for (const network of networks) { - const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid); - if (match) { - match.lastIpcObject = network; - } else { - rNetworks.push(apComp.createObject(root, { - lastIpcObject: network - })); - } - } - - // Check if pending connection succeeded after network list is fully updated - if (root.pendingConnection) { - Qt.callLater(() => { - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - if (connected) { - // Connection succeeded, stop timers and clear pending - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - root.pendingConnection = null; - } - }); - } - } - } - } - - Process { - id: getEthernetDevicesProc - - running: false - command: ["nmcli", "-g", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - onRunningChanged: { - root.ethernetProcessRunning = running; - } - onExited: { - Qt.callLater(() => { - const outputLength = ethernetStdout.text ? ethernetStdout.text.length : 0; - if (outputLength > 0) { - // Output was captured, process it - const output = ethernetStdout.text.trim(); - root.processEthernetOutput(output); - } - }); - } - stdout: StdioCollector { - id: ethernetStdout - onStreamFinished: { - const output = text.trim(); - - if (!output || output.length === 0) { - return; - } - - root.processEthernetOutput(output); - } - } - } - - function processEthernetOutput(output: string): void { - const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; - const rep = new RegExp("\\\\:", "g"); - const rep2 = new RegExp(PLACEHOLDER, "g"); - - const lines = output.split("\n"); - - const allDevices = lines.map(d => { - const dev = d.replace(rep, PLACEHOLDER).split(":"); - return { - interface: dev[0]?.replace(rep2, ":") ?? "", - type: dev[1]?.replace(rep2, ":") ?? "", - state: dev[2]?.replace(rep2, ":") ?? "", - connection: dev[3]?.replace(rep2, ":") ?? "" - }; - }); - - const ethernetOnly = allDevices.filter(d => d.type === "ethernet"); - - const ethernetDevices = ethernetOnly.map(d => { - const state = d.state || ""; - const connected = state === "100 (connected)" || state === "connected" || state.startsWith("connected"); - return { - interface: d.interface, - type: d.type, - state: state, - connection: d.connection, - connected: connected, - ipAddress: "", - gateway: "", - dns: [], - subnet: "", - macAddress: "", - speed: "" - }; - }); - - // Update the list - replace the entire array to ensure QML detects the change - // Create a new array and assign it to the property - const newDevices = []; - for (let i = 0; i < ethernetDevices.length; i++) { - newDevices.push(ethernetDevices[i]); - } - - // Replace the entire list - root.ethernetDevices = newDevices; - - // Force QML to detect the change by updating a property - root.ethernetDeviceCount = ethernetDevices.length; - } - - - Process { - id: connectEthernetProc - - onExited: { - getEthernetDevices(); - // Refresh device details after connection - Qt.callLater(() => { - const activeDevice = root.ethernetDevices.find(function(d) { return d.connected; }); - if (activeDevice && activeDevice.interface) { - updateEthernetDeviceDetails(activeDevice.interface); - } - }); - } - stdout: SplitParser { - onRead: getEthernetDevices() - } - stderr: StdioCollector { - onStreamFinished: { - } - } - } - - Process { - id: disconnectEthernetProc - - onExited: { - getEthernetDevices(); - // Clear device details after disconnection - Qt.callLater(() => { - root.ethernetDeviceDetails = null; - }); - } - stdout: SplitParser { - onRead: getEthernetDevices() - } - stderr: StdioCollector { - onStreamFinished: { - } - } - } - - Process { - id: getEthernetDetailsProc - - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - const output = text.trim(); - if (!output || output.length === 0) { - root.ethernetDeviceDetails = null; - return; - } - - const lines = output.split("\n"); - const details = { - ipAddress: "", - gateway: "", - dns: [], - subnet: "", - macAddress: "", - speed: "" - }; - - for (let i = 0; i < lines.length; i++) { -const line = lines[i]; - const parts = line.split(":"); - if (parts.length >= 2) { - const key = parts[0].trim(); - const value = parts.slice(1).join(":").trim(); - - if (key.startsWith("IP4.ADDRESS")) { - // Extract IP and subnet from format like "10.13.1.45/24" - const ipParts = value.split("/"); - details.ipAddress = ipParts[0] || ""; - if (ipParts[1]) { - // Convert CIDR notation to subnet mask - details.subnet = root.cidrToSubnetMask(ipParts[1]); - } else { - details.subnet = ""; - } - } else if (key === "IP4.GATEWAY") { - details.gateway = value; - } else if (key.startsWith("IP4.DNS")) { - details.dns.push(value); - } else if (key === "WIRED-PROPERTIES.MAC") { - details.macAddress = value; - } else if (key === "WIRED-PROPERTIES.SPEED") { - details.speed = value; - } - } - } - - root.ethernetDeviceDetails = details; - } - } - onExited: { - if (exitCode !== 0) { - root.ethernetDeviceDetails = null; - } - } - } - - Process { - id: findWirelessInterfaceProc - - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - const output = text.trim(); - if (!output || output.length === 0) { - root.wirelessDeviceDetails = null; - return; - } - - // Find the connected wifi interface from device status - const lines = output.split("\n"); - let wifiInterface = ""; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const parts = line.split(/\s+/); - // Format: DEVICE TYPE STATE CONNECTION - // Look for wifi devices that are connected - if (parts.length >= 3 && parts[1] === "wifi" && parts[2] === "connected") { - wifiInterface = parts[0]; - break; - } - } - - if (wifiInterface && wifiInterface.length > 0) { - getWirelessDetailsProc.exec(["nmcli", "device", "show", wifiInterface]); - } else { - root.wirelessDeviceDetails = null; - } - } - } - onExited: { - if (exitCode !== 0) { - root.wirelessDeviceDetails = null; - } - } - } - - Process { - id: getWirelessDetailsProc - - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - const output = text.trim(); - if (!output || output.length === 0) { - root.wirelessDeviceDetails = null; - return; - } - - const lines = output.split("\n"); - const details = { - ipAddress: "", - gateway: "", - dns: [], - subnet: "", - macAddress: "", - speed: "" - }; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const parts = line.split(":"); - if (parts.length >= 2) { - const key = parts[0].trim(); - const value = parts.slice(1).join(":").trim(); - - if (key.startsWith("IP4.ADDRESS")) { - // Extract IP and subnet from format like "10.13.1.45/24" - const ipParts = value.split("/"); - details.ipAddress = ipParts[0] || ""; - if (ipParts[1]) { - // Convert CIDR notation to subnet mask - details.subnet = root.cidrToSubnetMask(ipParts[1]); - } else { - details.subnet = ""; - } - } else if (key === "IP4.GATEWAY") { - details.gateway = value; - } else if (key.startsWith("IP4.DNS")) { - details.dns.push(value); - } else if (key === "GENERAL.HWADDR") { - details.macAddress = value; - } - } - } - - root.wirelessDeviceDetails = details; - } - } - onExited: { - if (exitCode !== 0) { - root.wirelessDeviceDetails = null; - } - } - } - - component AccessPoint: QtObject { - required property var lastIpcObject - readonly property string ssid: lastIpcObject.ssid - readonly property string bssid: lastIpcObject.bssid - readonly property int strength: lastIpcObject.strength - readonly property int frequency: lastIpcObject.frequency - readonly property bool active: lastIpcObject.active - readonly property string security: lastIpcObject.security - readonly property bool isSecure: security.length > 0 - } - - Component { - id: apComp - - AccessPoint {} - } } -- cgit v1.2.3-freya From 3f58823762ba6a894bb96fae4e9f06480714e460 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 11:12:07 -0500 Subject: nmcli: refactor to be readable/extensible --- services/Nmcli.qml | 511 ++++++++++++++++++++++++++++------------------------- 1 file changed, 267 insertions(+), 244 deletions(-) diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 5fb0c6c..a9f9e8e 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -32,6 +32,29 @@ Singleton { property list activeProcesses: [] property var debugLogger: null + // Constants + readonly property string deviceTypeWifi: "wifi" + readonly property string deviceTypeEthernet: "ethernet" + readonly property string connectionTypeWireless: "802-11-wireless" + readonly property string nmcliCommandDevice: "device" + readonly property string nmcliCommandConnection: "connection" + readonly property string nmcliCommandWifi: "wifi" + readonly property string nmcliCommandRadio: "radio" + readonly property string deviceStatusFields: "DEVICE,TYPE,STATE,CONNECTION" + readonly property string connectionListFields: "NAME,TYPE" + readonly property string wirelessSsidField: "802-11-wireless.ssid" + readonly property string networkListFields: "SSID,SIGNAL,SECURITY" + readonly property string networkDetailFields: "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY" + readonly property string securityKeyMgmt: "802-11-wireless-security.key-mgmt" + readonly property string securityPsk: "802-11-wireless-security.psk" + readonly property string keyMgmtWpaPsk: "wpa-psk" + readonly property string connectionParamType: "type" + readonly property string connectionParamConName: "con-name" + readonly property string connectionParamIfname: "ifname" + readonly property string connectionParamSsid: "ssid" + readonly property string connectionParamPassword: "password" + readonly property string connectionParamBssid: "802-11-wireless.bssid" + function setDebugLogger(logger: var): void { root.debugLogger = logger; } @@ -48,6 +71,128 @@ Singleton { log(message); } + function detectPasswordRequired(error: string): bool { + if (!error || error.length === 0) { + return false; + } + + return (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + } + + function parseNetworkOutput(output: string): list { + if (!output || output.length === 0) { + return []; + } + + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; + const rep = new RegExp("\\\\:", "g"); + const rep2 = new RegExp(PLACEHOLDER, "g"); + + const allNetworks = output.trim().split("\n") + .filter(line => line && line.length > 0) + .map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1] || "0", 10) || 0, + frequency: parseInt(net[2] || "0", 10) || 0, + ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), + bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), + security: (net[5] ?? "").trim() + }; + }) + .filter(n => n.ssid && n.ssid.length > 0); + + return allNetworks; + } + + function deduplicateNetworks(networks: list): list { + if (!networks || networks.length === 0) { + return []; + } + + const networkMap = new Map(); + for (const network of networks) { + const existing = networkMap.get(network.ssid); + if (!existing) { + networkMap.set(network.ssid, network); + } else { + if (network.active && !existing.active) { + networkMap.set(network.ssid, network); + } else if (!network.active && !existing.active) { + if (network.strength > existing.strength) { + networkMap.set(network.ssid, network); + } + } + } + } + + return Array.from(networkMap.values()); + } + + function isConnectionCommand(command: list): bool { + if (!command || command.length === 0) { + return false; + } + + return command.includes(root.nmcliCommandWifi) || command.includes(root.nmcliCommandConnection); + } + + function parseDeviceStatusOutput(output: string, filterType: string): list { + if (!output || output.length === 0) { + return []; + } + + const interfaces = []; + const lines = output.trim().split("\n"); + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2) { + const deviceType = parts[1]; + let shouldInclude = false; + + if (filterType === root.deviceTypeWifi && deviceType === root.deviceTypeWifi) { + shouldInclude = true; + } else if (filterType === root.deviceTypeEthernet && deviceType === root.deviceTypeEthernet) { + shouldInclude = true; + } else if (filterType === "both" && (deviceType === root.deviceTypeWifi || deviceType === root.deviceTypeEthernet)) { + shouldInclude = true; + } + + if (shouldInclude) { + interfaces.push({ + device: parts[0] || "", + type: parts[1] || "", + state: parts[2] || "", + connection: parts[3] || "" + }); + } + } + } + + return interfaces; + } + + function isConnectedState(state: string): bool { + if (!state || state.length === 0) { + return false; + } + + return state === "100 (connected)" || + state === "connected" || + state.startsWith("connected"); + } + function executeCommand(args: list, callback: var): void { const proc = commandProc.createObject(root); proc.command = ["nmcli", ...args]; @@ -68,68 +213,42 @@ Singleton { } function getDeviceStatus(callback: var): void { - executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { if (callback) callback(result.output); }); } function getWirelessInterfaces(callback: var): void { - executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { - const interfaces = []; - const lines = result.output.trim().split("\n"); - for (const line of lines) { - const parts = line.split(":"); - if (parts.length >= 2 && parts[1] === "wifi") { - interfaces.push({ - device: parts[0] || "", - type: parts[1] || "", - state: parts[2] || "", - connection: parts[3] || "" - }); - } - } + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeWifi); root.wirelessInterfaces = interfaces; if (callback) callback(interfaces); }); } function getEthernetInterfaces(callback: var): void { - executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { - const interfaces = []; + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeEthernet); const devices = []; - const lines = result.output.trim().split("\n"); - for (const line of lines) { - const parts = line.split(":"); - if (parts.length >= 2 && parts[1] === "ethernet") { - const device = parts[0] || ""; - const type = parts[1] || ""; - const state = parts[2] || ""; - const connection = parts[3] || ""; - - const connected = state === "100 (connected)" || state === "connected" || state.startsWith("connected"); - - interfaces.push({ - device: device, - type: type, - state: state, - connection: connection - }); - - devices.push({ - interface: device, - type: type, - state: state, - connection: connection, - connected: connected, - ipAddress: "", - gateway: "", - dns: [], - subnet: "", - macAddress: "", - speed: "" - }); - } + + for (const iface of interfaces) { + const connected = isConnectedState(iface.state); + + devices.push({ + interface: iface.device, + type: iface.type, + state: iface.state, + connection: iface.connection, + connected: connected, + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }); } + root.ethernetInterfaces = interfaces; root.ethernetDevices = devices; if (callback) callback(interfaces); @@ -138,7 +257,7 @@ Singleton { function connectEthernet(connectionName: string, interfaceName: string, callback: var): void { if (connectionName && connectionName.length > 0) { - executeCommand(["connection", "up", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "up", connectionName], (result) => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); @@ -152,7 +271,7 @@ Singleton { if (callback) callback(result); }); } else if (interfaceName && interfaceName.length > 0) { - executeCommand(["device", "connect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], (result) => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); @@ -174,7 +293,7 @@ Singleton { return; } - executeCommand(["connection", "down", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "down", connectionName], (result) => { if (result.success) { root.ethernetDeviceDetails = null; Qt.callLater(() => { @@ -186,31 +305,19 @@ Singleton { } function getAllInterfaces(callback: var): void { - executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { - const interfaces = []; - const lines = result.output.trim().split("\n"); - for (const line of lines) { - const parts = line.split(":"); - if (parts.length >= 2 && (parts[1] === "wifi" || parts[1] === "ethernet")) { - interfaces.push({ - device: parts[0] || "", - type: parts[1] || "", - state: parts[2] || "", - connection: parts[3] || "" - }); - } - } + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + const interfaces = parseDeviceStatusOutput(result.output, "both"); if (callback) callback(interfaces); }); } function isInterfaceConnected(interfaceName: string, callback: var): void { - executeCommand(["device", "status"], (result) => { + executeCommand([root.nmcliCommandDevice, "status"], (result) => { const lines = result.output.trim().split("\n"); for (const line of lines) { const parts = line.split(/\s+/); if (parts.length >= 3 && parts[0] === interfaceName) { - const connected = parts[2] === "connected" || parts[2].startsWith("connected"); + const connected = isConnectedState(parts[2]); if (callback) callback(connected); return; } @@ -258,9 +365,9 @@ Singleton { return; } - let cmd = ["device", "wifi", "connect", ssid]; + let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid]; if (password && password.length > 0) { - cmd.push("password", password); + cmd.push(root.connectionParamPassword, password); } executeCommand(cmd, (result) => { if (result.needsPassword && callback) { @@ -283,14 +390,14 @@ Singleton { function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void { checkAndDeleteConnection(ssid, () => { - const cmd = ["connection", "add", - "type", "wifi", - "con-name", ssid, - "ifname", "*", - "ssid", ssid, - "802-11-wireless.bssid", bssidUpper, - "802-11-wireless-security.key-mgmt", "wpa-psk", - "802-11-wireless-security.psk", password]; + const cmd = [root.nmcliCommandConnection, "add", + root.connectionParamType, root.deviceTypeWifi, + root.connectionParamConName, ssid, + root.connectionParamIfname, "*", + root.connectionParamSsid, ssid, + root.connectionParamBssid, bssidUpper, + root.securityKeyMgmt, root.keyMgmtWpaPsk, + root.securityPsk, password]; executeCommand(cmd, (result) => { if (result.success) { @@ -307,7 +414,7 @@ Singleton { activateConnection(ssid, callback); } else { log("Connection profile creation failed, trying fallback..."); - let fallbackCmd = ["device", "wifi", "connect", ssid, "password", password]; + let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid, root.connectionParamPassword, password]; executeCommand(fallbackCmd, (fallbackResult) => { if (callback) callback(fallbackResult); }); @@ -318,9 +425,9 @@ Singleton { } function checkAndDeleteConnection(ssid: string, callback: var): void { - executeCommand(["connection", "show", ssid], (result) => { + executeCommand([root.nmcliCommandConnection, "show", ssid], (result) => { if (result.success) { - executeCommand(["connection", "delete", ssid], (deleteResult) => { + executeCommand([root.nmcliCommandConnection, "delete", ssid], (deleteResult) => { Qt.callLater(() => { if (callback) callback(); }, 300); @@ -332,13 +439,13 @@ Singleton { } function activateConnection(connectionName: string, callback: var): void { - executeCommand(["connection", "up", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "up", connectionName], (result) => { if (callback) callback(result); }); } function loadSavedConnections(callback: var): void { - executeCommand(["-t", "-f", "NAME,TYPE", "connection", "show"], (result) => { + executeCommand(["-t", "-f", root.connectionListFields, root.nmcliCommandConnection, "show"], (result) => { if (!result.success) { root.savedConnections = []; root.savedConnectionSsids = []; @@ -362,7 +469,7 @@ Singleton { const type = parts[1]; connections.push(name); - if (type === "802-11-wireless") { + if (type === root.connectionTypeWireless) { wifiConnections.push(name); } } @@ -387,7 +494,7 @@ Singleton { const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; root.currentSsidQueryIndex++; - executeCommand(["-t", "-f", "802-11-wireless.ssid", "connection", "show", connectionName], (result) => { + executeCommand(["-t", "-f", root.wirelessSsidField, root.nmcliCommandConnection, "show", connectionName], (result) => { if (result.success) { processSsidOutput(result.output); } @@ -456,7 +563,7 @@ Singleton { conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() ) || ssid; - executeCommand(["connection", "delete", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "delete", connectionName], (result) => { if (result.success) { Qt.callLater(() => { loadSavedConnections(() => {}); @@ -468,11 +575,11 @@ Singleton { function disconnect(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand(["device", "disconnect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], (result) => { if (callback) callback(result.success ? result.output : ""); }); } else { - executeCommand(["device", "disconnect", "wifi"], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], (result) => { if (callback) callback(result.success ? result.output : ""); }); } @@ -480,13 +587,13 @@ Singleton { function disconnectFromNetwork(): void { if (active && active.ssid) { - executeCommand(["connection", "down", active.ssid], (result) => { + executeCommand([root.nmcliCommandConnection, "down", active.ssid], (result) => { if (result.success) { getNetworks(() => {}); } }); } else { - executeCommand(["device", "disconnect", "wifi"], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], (result) => { if (result.success) { getNetworks(() => {}); } @@ -495,7 +602,7 @@ Singleton { } function getDeviceDetails(interfaceName: string, callback: var): void { - executeCommand(["device", "show", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "show", interfaceName], (result) => { if (callback) callback(result.output); }); } @@ -511,7 +618,7 @@ Singleton { const parts = line.split(":"); if (parts.length >= 4) { const state = parts[2] || ""; - if (state === "connected" || state.startsWith("connected")) { + if (isConnectedState(state)) { connected = true; activeIf = parts[0] || ""; activeConn = parts[3] || ""; @@ -530,7 +637,7 @@ Singleton { function bringInterfaceUp(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand(["device", "connect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], (result) => { if (callback) { callback(result); } @@ -542,7 +649,7 @@ Singleton { function bringInterfaceDown(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand(["device", "disconnect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], (result) => { if (callback) { callback(result); } @@ -553,9 +660,9 @@ Singleton { } function scanWirelessNetworks(interfaceName: string, callback: var): void { - let cmd = ["device", "wifi", "rescan"]; + let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "rescan"]; if (interfaceName && interfaceName.length > 0) { - cmd.push("ifname", interfaceName); + cmd.push(root.connectionParamIfname, interfaceName); } executeCommand(cmd, (result) => { if (callback) { @@ -570,7 +677,7 @@ Singleton { function enableWifi(enabled: bool, callback: var): void { const cmd = enabled ? "on" : "off"; - executeCommand(["radio", "wifi", cmd], (result) => { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, cmd], (result) => { if (result.success) { getWifiStatus((status) => { root.wifiEnabled = status; @@ -588,7 +695,7 @@ Singleton { } function getWifiStatus(callback: var): void { - executeCommand(["radio", "wifi"], (result) => { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi], (result) => { if (result.success) { const enabled = result.output.trim() === "enabled"; root.wifiEnabled = enabled; @@ -600,48 +707,14 @@ Singleton { } function getNetworks(callback: var): void { - executeCommand(["-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"], (result) => { + executeCommand(["-g", root.networkDetailFields, "d", "w"], (result) => { if (!result.success) { if (callback) callback([]); return; } - const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; - const rep = new RegExp("\\\\:", "g"); - const rep2 = new RegExp(PLACEHOLDER, "g"); - - const allNetworks = result.output.trim().split("\n") - .filter(line => line && line.length > 0) - .map(n => { - const net = n.replace(rep, PLACEHOLDER).split(":"); - return { - active: net[0] === "yes", - strength: parseInt(net[1] || "0", 10) || 0, - frequency: parseInt(net[2] || "0", 10) || 0, - ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), - bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), - security: (net[5] ?? "").trim() - }; - }) - .filter(n => n.ssid && n.ssid.length > 0); - - const networkMap = new Map(); - for (const network of allNetworks) { - const existing = networkMap.get(network.ssid); - if (!existing) { - networkMap.set(network.ssid, network); - } else { - if (network.active && !existing.active) { - networkMap.set(network.ssid, network); - } else if (!network.active && !existing.active) { - if (network.strength > existing.strength) { - networkMap.set(network.ssid, network); - } - } - } - } - - const networks = Array.from(networkMap.values()); + const allNetworks = parseNetworkOutput(result.output); + const networks = deduplicateNetworks(allNetworks); const rNetworks = root.networks; const destroyed = rNetworks.filter(rn => !networks.find(n => @@ -678,9 +751,9 @@ Singleton { } function getWirelessSSIDs(interfaceName: string, callback: var): void { - let cmd = ["-t", "-f", "SSID,SIGNAL,SECURITY", "device", "wifi", "list"]; + let cmd = ["-t", "-f", root.networkListFields, root.nmcliCommandDevice, root.nmcliCommandWifi, "list"]; if (interfaceName && interfaceName.length > 0) { - cmd.push("ifname", interfaceName); + cmd.push(root.connectionParamIfname, interfaceName); } executeCommand(cmd, (result) => { if (!result.success) { @@ -721,6 +794,43 @@ Singleton { }); } + function handlePasswordRequired(proc: var, error: string, output: string, exitCode: int): bool { + if (!proc || !error || error.length === 0) { + return false; + } + + if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) { + return false; + } + + const needsPassword = detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: output || "", + error: error, + exitCode: exitCode, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return true; + } + + return false; + } + component CommandProcess: Process { id: proc property var callback: null @@ -745,43 +855,8 @@ Singleton { onStreamFinished: { const error = text.trim(); if (error && error.length > 0) { - const isConnectionCommand = proc.command && proc.command.length > 0 && - (proc.command.includes("wifi") || proc.command.includes("connection")); - - if (isConnectionCommand && root.pendingConnection && root.pendingConnection.callback) { - const needsPassword = (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); - - if (needsPassword && !proc.callbackCalled && root.pendingConnection) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - const pending = root.pendingConnection; - root.pendingConnection = null; - proc.callbackCalled = true; - const result = { - success: false, - output: (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : "", - error: error, - exitCode: -1, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } - } - } + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + handlePasswordRequired(proc, error, output, -1); } } } @@ -798,44 +873,16 @@ Singleton { const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; const success = proc.exitCode === 0; + const cmdIsConnection = isConnectionCommand(proc.command); - const isConnectionCommand = proc.command && proc.command.length > 0 && - (proc.command.includes("wifi") || proc.command.includes("connection")); - const needsPassword = isConnectionCommand && error && error.length > 0 && - (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); - - if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - const pending = root.pendingConnection; - root.pendingConnection = null; - proc.callbackCalled = true; - const result = { - success: false, - output: output, - error: error, - exitCode: proc.exitCode, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } + if (handlePasswordRequired(proc, error, output, proc.exitCode)) { proc.processFinished(); return; - } else if (!success && isConnectionCommand && root.pendingConnection) { + } + + const needsPassword = cmdIsConnection && detectPasswordRequired(error); + + if (!success && cmdIsConnection && root.pendingConnection) { const failedSsid = root.pendingConnection.ssid; root.connectionFailed(failedSsid); } @@ -891,20 +938,8 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - const isConnectionCommand = proc.command && proc.command.length > 0 && - (proc.command.includes("wifi") || proc.command.includes("connection")); - - if (isConnectionCommand) { - const needsPassword = (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); + if (isConnectionCommand(proc.command)) { + const needsPassword = detectPasswordRequired(error); if (needsPassword && !proc.callbackCalled && root.pendingConnection) { const pending = root.pendingConnection; @@ -983,20 +1018,8 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - const isConnectionCommand = proc.command && proc.command.length > 0 && - (proc.command.includes("wifi") || proc.command.includes("connection")); - - if (isConnectionCommand) { - const needsPassword = (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); + if (isConnectionCommand(proc.command)) { + const needsPassword = detectPasswordRequired(error); if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { connectionCheckTimer.stop(); @@ -1076,7 +1099,7 @@ Singleton { function getWirelessDeviceDetails(interfaceName: string, callback: var): void { if (!interfaceName || interfaceName.length === 0) { const activeInterface = root.wirelessInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; @@ -1102,7 +1125,7 @@ Singleton { function getEthernetDeviceDetails(interfaceName: string, callback: var): void { if (!interfaceName || interfaceName.length === 0) { const activeInterface = root.ethernetInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; @@ -1179,7 +1202,7 @@ Singleton { Process { id: rescanProc - command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"] + command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] onExited: { getNetworks(() => {}); } @@ -1217,7 +1240,7 @@ Singleton { Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { const activeWireless = root.wirelessInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeWireless && activeWireless.device) { getWirelessDeviceDetails(activeWireless.device, () => {}); @@ -1226,7 +1249,7 @@ Singleton { if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeEthernet && activeEthernet.device) { getEthernetDeviceDetails(activeEthernet.device, () => {}); @@ -1258,7 +1281,7 @@ Singleton { Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { const activeWireless = root.wirelessInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeWireless && activeWireless.device) { getWirelessDeviceDetails(activeWireless.device, () => {}); @@ -1267,7 +1290,7 @@ Singleton { if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeEthernet && activeEthernet.device) { getEthernetDeviceDetails(activeEthernet.device, () => {}); -- cgit v1.2.3-freya From 540cebdbbdafe51e0f946e7a256320537a332612 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 11:59:36 -0500 Subject: controlcenter: merging wireless/ethernet panes (wip/stash) --- modules/controlcenter/NavRail.qml | 5 + modules/controlcenter/Panes.qml | 7 + modules/controlcenter/Session.qml | 2 +- modules/controlcenter/network/NetworkingPane.qml | 579 +++++++++++++++++++++ .../controlcenter/network/NetworkingSettings.qml | 106 ++++ 5 files changed, 698 insertions(+), 1 deletion(-) create mode 100644 modules/controlcenter/network/NetworkingPane.qml create mode 100644 modules/controlcenter/network/NetworkingSettings.qml diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index ca36048..abe2ea2 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -191,6 +191,11 @@ Item { icon: "apps" label: "launcher" } + + NavItem { + icon: "router" + label: "networking" + } } component NavItem: Item { diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 11e60d3..0958902 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -76,6 +76,13 @@ ClippingRectangle { } } + Pane { + index: 7 + sourceComponent: NetworkingPane { + session: root.session + } + } + Behavior on y { Anim {} } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index d44898e..1ff9688 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -2,7 +2,7 @@ import Quickshell.Bluetooth import QtQuick QtObject { - readonly property list panes: ["ethernet", "wireless", "bluetooth", "audio", "appearance", "taskbar", "launcher"] + readonly property list panes: ["ethernet", "wireless", "bluetooth", "audio", "appearance", "taskbar", "launcher", "networking"] required property var root property bool floating: false diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml new file mode 100644 index 0000000..f220960 --- /dev/null +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -0,0 +1,579 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../ethernet" +import "." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property Session session + + anchors.fill: parent + + spacing: 0 + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true + + // Left pane - networking list with collapsible sections + StyledFlickable { + 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 + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContent.height + + ColumnLayout { + id: leftContent + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + + // Settings header above the collapsible sections + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + ToggleButton { + toggled: Nmcli.wifiEnabled + icon: "wifi" + accent: "Tertiary" + + onClicked: { + Nmcli.toggleWifi(null); + } + } + + ToggleButton { + toggled: Nmcli.scanning + icon: "wifi_find" + accent: "Secondary" + + onClicked: { + Nmcli.rescanWifi(); + } + } + + ToggleButton { + toggled: !root.session.ethernet.active && !root.session.network.active + icon: "settings" + accent: "Primary" + + onClicked: { + if (root.session.ethernet.active || root.session.network.active) { + root.session.ethernet.active = null; + root.session.network.active = null; + } else { + // Toggle to show settings - prefer ethernet if available, otherwise wireless + if (Nmcli.ethernetDevices.length > 0) { + root.session.ethernet.active = ethernetListView.model.get(0)?.modelData ?? null; + } else if (Nmcli.networks.length > 0) { + root.session.network.active = wirelessListView.model.get(0)?.modelData ?? null; + } + } + } + } + } + + CollapsibleSection { + id: ethernetListSection + + Layout.fillWidth: true + title: qsTr("Ethernet") + expanded: true + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + } + + StyledText { + Layout.fillWidth: true + text: qsTr("All available ethernet devices") + color: Colours.palette.m3outline + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Nmcli.ethernetDevices.length > 0 ? Math.min(400, Math.max(200, Nmcli.ethernetDevices.length * 80)) : 200 + + StyledListView { + id: ethernetListView + + anchors.fill: parent + + model: Nmcli.ethernetDevices + + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: ethernetListView + } + + delegate: StyledRect { + required property var modelData + + anchors.left: parent.left + anchors.right: parent.right + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: root.session.ethernet.active === modelData ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + root.session.ethernet.active = modelData; + } + } + + RowLayout { + id: rowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: "cable" + font.pointSize: Appearance.font.size.large + fill: modelData.connected ? 1 : 0 + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + + text: modelData.interface || qsTr("Unknown") + } + + StyledText { + text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.connected ? 500 : 400 + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) + + StateLayer { + function onClicked(): void { + if (modelData.connected && modelData.connection) { + Nmcli.disconnectEthernet(modelData.connection, () => {}); + } else { + Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); + } + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + text: modelData.connected ? "link_off" : "link" + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + } + + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + } + } + } + + CollapsibleSection { + id: wirelessListSection + + Layout.fillWidth: true + title: qsTr("Wireless") + expanded: true + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Networks (%1)").arg(Nmcli.networks.length) + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + visible: Nmcli.scanning + text: qsTr("Scanning...") + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.small + } + } + + StyledText { + Layout.fillWidth: true + text: qsTr("All available WiFi networks") + color: Colours.palette.m3outline + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Nmcli.networks.length > 0 ? Math.min(400, Math.max(200, Nmcli.networks.length * 80)) : 200 + + StyledListView { + id: wirelessListView + + anchors.fill: parent + + model: Nmcli.networks + + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: wirelessListView + } + + delegate: StyledRect { + required property var modelData + + anchors.left: parent.left + anchors.right: parent.right + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.network.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: root.session.network.active === modelData ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + root.session.network.active = modelData; + // Check if we need to refresh saved connections when selecting a network + if (modelData && modelData.ssid) { + checkSavedProfileForNetwork(modelData.ssid); + } + } + } + + RowLayout { + id: wirelessRowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: wirelessIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + MaterialIcon { + id: wirelessIcon + + anchors.centerIn: parent + text: modelData.isSecure ? "lock" : "wifi" + font.pointSize: Appearance.font.size.large + fill: modelData.active ? 1 : 0 + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + + text: modelData.ssid || qsTr("Unknown") + } + + StyledText { + text: modelData.active ? qsTr("Connected") : (modelData.isSecure ? qsTr("Secured") : qsTr("Open")) + color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.active ? 500 : 400 + } + + StyledText { + text: qsTr("%1%").arg(modelData.strength) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) + + StateLayer { + function onClicked(): void { + if (modelData.active) { + Nmcli.disconnectFromNetwork(); + } else { + handleWirelessConnect(modelData); + } + } + } + + MaterialIcon { + id: wirelessConnectIcon + + anchors.centerIn: parent + text: modelData.active ? "link_off" : "link" + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + } + + implicitHeight: wirelessRowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + } + } + } + } + } + + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ClippingRectangle { + 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 + 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 || "") : "") + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + + clip: false + asynchronous: true + sourceComponent: pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings + + 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 + } + } + PropertyAction {} + 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 + } + } + } + } + + onPaneChanged: { + paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + } + } + } + + InnerBorder { + id: rightBorder + + leftThickness: Appearance.padding.normal / 2 + } + + Component { + id: settings + + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + + NetworkingSettings { + id: settingsInner + + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } + } + } + + Component { + id: ethernetDetails + + EthernetDetails { + session: root.session + } + } + + Component { + id: wirelessDetails + + WirelessDetails { + session: root.session + } + } + } + + WirelessPasswordDialog { + anchors.fill: parent + session: root.session + z: 1000 + } + + component Anim: NumberAnimation { + target: loader + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + } + + function checkSavedProfileForNetwork(ssid: string): void { + if (ssid && ssid.length > 0) { + Nmcli.loadSavedConnections(() => {}); + } + } + + function handleWirelessConnect(network): void { + if (Nmcli.active && Nmcli.active.ssid !== network.ssid) { + Nmcli.disconnectFromNetwork(); + Qt.callLater(() => { + connectToWirelessNetwork(network); + }); + } else { + connectToWirelessNetwork(network); + } + } + + function connectToWirelessNetwork(network): void { + if (network.isSecure) { + const hasSavedProfile = Nmcli.hasSavedProfile(network.ssid); + + if (hasSavedProfile) { + Nmcli.connectToNetwork(network.ssid, "", network.bssid, null); + } else { + Nmcli.connectToNetworkWithPasswordCheck( + network.ssid, + network.isSecure, + (result) => { + if (result.needsPassword) { + if (Nmcli.pendingConnection) { + Nmcli.connectionCheckTimer.stop(); + Nmcli.immediateCheckTimer.stop(); + Nmcli.immediateCheckTimer.checkCount = 0; + Nmcli.pendingConnection = null; + } + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = network; + } + }, + network.bssid + ); + } + } else { + Nmcli.connectToNetwork(network.ssid, "", network.bssid, null); + } + } +} + diff --git a/modules/controlcenter/network/NetworkingSettings.qml b/modules/controlcenter/network/NetworkingSettings.qml new file mode 100644 index 0000000..2475fed --- /dev/null +++ b/modules/controlcenter/network/NetworkingSettings.qml @@ -0,0 +1,106 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "router" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Networking settings") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Ethernet") + description: qsTr("Ethernet device information") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Total devices") + value: qsTr("%1").arg(Nmcli.ethernetDevices.length) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Connected devices") + value: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Wireless") + description: qsTr("WiFi network settings") + } + + SectionContainer { + ToggleRow { + label: qsTr("WiFi enabled") + checked: Nmcli.wifiEnabled + toggle.onToggled: { + Nmcli.enableWifi(checked); + } + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Current connection") + description: qsTr("Active network connection information") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Network") + value: Nmcli.active ? Nmcli.active.ssid : (Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : qsTr("Not connected")) + } + + PropertyRow { + showTopMargin: true + visible: Nmcli.active !== null + label: qsTr("Signal strength") + value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + visible: Nmcli.active !== null + label: qsTr("Security") + value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + visible: Nmcli.active !== null + label: qsTr("Frequency") + value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A") + } + } +} + -- cgit v1.2.3-freya From 6043adf66615e3354a30f7efe24a1332bd10e3e5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 12:05:07 -0500 Subject: controlcenter: merged wireless/ethernet panes --- modules/controlcenter/NavRail.qml | 14 +- modules/controlcenter/network/NetworkingPane.qml | 188 ++++++++++------------- 2 files changed, 82 insertions(+), 120 deletions(-) diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index abe2ea2..80a1ee3 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -158,13 +158,8 @@ Item { NavItem { Layout.topMargin: Appearance.spacing.large * 2 - icon: "cable" - label: "ethernet" - } - - NavItem { - icon: "wifi" - label: "wireless" + icon: "router" + label: "networking" } NavItem { @@ -191,11 +186,6 @@ Item { icon: "apps" label: "launcher" } - - NavItem { - icon: "router" - label: "networking" - } } component NavItem: Item { diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index f220960..8616c85 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -90,9 +90,9 @@ RowLayout { } else { // Toggle to show settings - prefer ethernet if available, otherwise wireless if (Nmcli.ethernetDevices.length > 0) { - root.session.ethernet.active = ethernetListView.model.get(0)?.modelData ?? null; + root.session.ethernet.active = Nmcli.ethernetDevices[0]; } else if (Nmcli.networks.length > 0) { - root.session.network.active = wirelessListView.model.get(0)?.modelData ?? null; + root.session.network.active = Nmcli.networks[0]; } } } @@ -127,29 +127,16 @@ RowLayout { color: Colours.palette.m3outline } - Item { - Layout.fillWidth: true - Layout.preferredHeight: Nmcli.ethernetDevices.length > 0 ? Math.min(400, Math.max(200, Nmcli.ethernetDevices.length * 80)) : 200 - - StyledListView { - id: ethernetListView - - anchors.fill: parent + Repeater { + id: ethernetRepeater + Layout.fillWidth: true model: Nmcli.ethernetDevices - spacing: Appearance.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: ethernetListView - } - - delegate: StyledRect { + delegate: StyledRect { required property var modelData - anchors.left: parent.left - anchors.right: parent.right + Layout.fillWidth: true color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal @@ -233,7 +220,6 @@ RowLayout { } implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 - } } } } @@ -274,123 +260,109 @@ RowLayout { color: Colours.palette.m3outline } - Item { - Layout.fillWidth: true - Layout.preferredHeight: Nmcli.networks.length > 0 ? Math.min(400, Math.max(200, Nmcli.networks.length * 80)) : 200 + Repeater { + id: wirelessRepeater - StyledListView { - id: wirelessListView + Layout.fillWidth: true + model: Nmcli.networks - anchors.fill: parent + delegate: StyledRect { + required property var modelData - model: Nmcli.networks + Layout.fillWidth: true - spacing: Appearance.spacing.small / 2 - clip: true + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.network.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: root.session.network.active === modelData ? 1 : 0 + border.color: Colours.palette.m3primary - StyledScrollBar.vertical: StyledScrollBar { - flickable: wirelessListView + StateLayer { + function onClicked(): void { + root.session.network.active = modelData; + // Check if we need to refresh saved connections when selecting a network + if (modelData && modelData.ssid) { + checkSavedProfileForNetwork(modelData.ssid); + } + } } - delegate: StyledRect { - required property var modelData + RowLayout { + id: wirelessRowLayout anchors.left: parent.left anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.network.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: root.session.network.active === modelData ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - root.session.network.active = modelData; - // Check if we need to refresh saved connections when selecting a network - if (modelData && modelData.ssid) { - checkSavedProfileForNetwork(modelData.ssid); - } - } - } - - RowLayout { - id: wirelessRowLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledRect { - implicitWidth: implicitHeight - implicitHeight: wirelessIcon.implicitHeight + Appearance.padding.normal * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: wirelessIcon.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.normal - color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.normal + color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - MaterialIcon { - id: wirelessIcon + MaterialIcon { + id: wirelessIcon - anchors.centerIn: parent - text: modelData.isSecure ? "lock" : "wifi" - font.pointSize: Appearance.font.size.large - fill: modelData.active ? 1 : 0 - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } + anchors.centerIn: parent + text: modelData.isSecure ? "lock" : "wifi" + font.pointSize: Appearance.font.size.large + fill: modelData.active ? 1 : 0 + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } + } - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 - text: modelData.ssid || qsTr("Unknown") - } + text: modelData.ssid || qsTr("Unknown") + } - StyledText { - text: modelData.active ? qsTr("Connected") : (modelData.isSecure ? qsTr("Secured") : qsTr("Open")) - color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: modelData.active ? 500 : 400 - } + StyledText { + text: modelData.active ? qsTr("Connected") : (modelData.isSecure ? qsTr("Secured") : qsTr("Open")) + color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.active ? 500 : 400 + } - StyledText { - text: qsTr("%1%").arg(modelData.strength) - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } + StyledText { + text: qsTr("%1%").arg(modelData.strength) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } - StyledRect { - implicitWidth: implicitHeight - implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2 - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) - StateLayer { - function onClicked(): void { - if (modelData.active) { - Nmcli.disconnectFromNetwork(); - } else { - handleWirelessConnect(modelData); - } + StateLayer { + function onClicked(): void { + if (modelData.active) { + Nmcli.disconnectFromNetwork(); + } else { + handleWirelessConnect(modelData); } } + } - MaterialIcon { - id: wirelessConnectIcon + MaterialIcon { + id: wirelessConnectIcon - anchors.centerIn: parent - text: modelData.active ? "link_off" : "link" - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } + anchors.centerIn: parent + text: modelData.active ? "link_off" : "link" + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } - - implicitHeight: wirelessRowLayout.implicitHeight + Appearance.padding.normal * 2 } + + implicitHeight: wirelessRowLayout.implicitHeight + Appearance.padding.normal * 2 } } } -- cgit v1.2.3-freya From 28ebba0c1b10b03647706928e8b31b099aa12b97 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 13:41:57 -0500 Subject: controlcenter: fixed default pane --- modules/controlcenter/Session.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index 1ff9688..d09d977 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -6,8 +6,8 @@ QtObject { required property var root property bool floating: false - property string active: panes[0] - property int activeIndex: 0 + property string active: "networking" + property int activeIndex: 7 property bool navExpanded: false readonly property Bt bt: Bt {} -- cgit v1.2.3-freya From ebc2772b3ef54e9f93865ad5c497665b9fffe1cd Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 13:44:15 -0500 Subject: controlcenter: corrected index of panes --- modules/controlcenter/Panes.qml | 25 +++++-------------------- modules/controlcenter/Session.qml | 4 ++-- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 0958902..8a46088 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -1,6 +1,5 @@ pragma ComponentBehavior: Bound -import "ethernet" import "bluetooth" import "network" import "audio" @@ -29,60 +28,46 @@ ClippingRectangle { Pane { index: 0 - sourceComponent: EthernetPane { + sourceComponent: NetworkingPane { session: root.session } } Pane { index: 1 - sourceComponent: WirelessPane { - session: root.session - } - } - - Pane { - index: 2 sourceComponent: BtPane { session: root.session } } Pane { - index: 3 + index: 2 sourceComponent: AudioPane { session: root.session } } Pane { - index: 4 + index: 3 sourceComponent: AppearancePane { session: root.session } } Pane { - index: 5 + index: 4 sourceComponent: TaskbarPane { session: root.session } } Pane { - index: 6 + index: 5 sourceComponent: LauncherPane { session: root.session } } - Pane { - index: 7 - sourceComponent: NetworkingPane { - session: root.session - } - } - Behavior on y { Anim {} } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index d09d977..0aad702 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -2,12 +2,12 @@ import Quickshell.Bluetooth import QtQuick QtObject { - readonly property list panes: ["ethernet", "wireless", "bluetooth", "audio", "appearance", "taskbar", "launcher", "networking"] + readonly property list panes: ["networking", "bluetooth", "audio", "appearance", "taskbar", "launcher"] required property var root property bool floating: false property string active: "networking" - property int activeIndex: 7 + property int activeIndex: 0 property bool navExpanded: false readonly property Bt bt: Bt {} -- cgit v1.2.3-freya From c30022b75e811488928c9cb6a0c6a3ebeeb6b0e3 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 13:48:32 -0500 Subject: controlcenter: fix bug, couldn't select wifi network while ethernet interface was selected --- modules/controlcenter/network/NetworkingPane.qml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 8616c85..5271c56 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -145,6 +145,7 @@ RowLayout { StateLayer { function onClicked(): void { + root.session.network.active = null; root.session.ethernet.active = modelData; } } @@ -276,15 +277,16 @@ RowLayout { border.width: root.session.network.active === modelData ? 1 : 0 border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - root.session.network.active = modelData; - // Check if we need to refresh saved connections when selecting a network - if (modelData && modelData.ssid) { - checkSavedProfileForNetwork(modelData.ssid); + StateLayer { + function onClicked(): void { + root.session.ethernet.active = null; + root.session.network.active = modelData; + // Check if we need to refresh saved connections when selecting a network + if (modelData && modelData.ssid) { + checkSavedProfileForNetwork(modelData.ssid); + } } } - } RowLayout { id: wirelessRowLayout -- cgit v1.2.3-freya From f39c4b6e6c446bf503acaf1cb5468c3e98b46d52 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 13:55:12 -0500 Subject: controlcenter: fix bug, connection information update event --- modules/controlcenter/network/WirelessDetails.qml | 54 ++++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index ff86b8a..334c4b3 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -23,6 +23,11 @@ Item { } onNetworkChanged: { + // Restart timer when network changes + connectionUpdateTimer.stop(); + if (network && network.ssid) { + connectionUpdateTimer.start(); + } updateDeviceDetails(); checkSavedProfile(); } @@ -38,11 +43,56 @@ Item { function onActiveChanged() { updateDeviceDetails(); } + function onWirelessDeviceDetailsChanged() { + // When details are updated, check if we should stop the timer + if (network && network.ssid) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { + // We have details for the active network, stop the timer + connectionUpdateTimer.stop(); + } + } + } + } + + Timer { + id: connectionUpdateTimer + interval: 500 + repeat: true + running: network && network.ssid + onTriggered: { + // Periodically check if network becomes active and update details + if (network) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (isActive) { + // Network is active - check if we have details + if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { + // Network is active but we don't have details yet, fetch them + Nmcli.getWirelessDeviceDetails("", () => { + // After fetching, check if we got details - if not, timer will try again + }); + } else { + // We have details, can stop the timer + connectionUpdateTimer.stop(); + } + } else { + // Network is not active, clear details + if (Nmcli.wirelessDeviceDetails !== null) { + Nmcli.wirelessDeviceDetails = null; + } + } + } + } } function updateDeviceDetails(): void { - if (network && Nmcli.active && Nmcli.active.ssid === network.ssid) { - Nmcli.getWirelessDeviceDetails("", () => {}); + if (network && network.ssid) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (isActive) { + Nmcli.getWirelessDeviceDetails("", () => {}); + } else { + Nmcli.wirelessDeviceDetails = null; + } } else { Nmcli.wirelessDeviceDetails = null; } -- cgit v1.2.3-freya From 9cdc30058a0d53602f6c065315b3956497a2f68c Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 14:03:10 -0500 Subject: controlcenter: font size adjustments on headings --- components/controls/CollapsibleSection.qml | 2 +- modules/controlcenter/audio/AudioPane.qml | 4 ++-- modules/controlcenter/bluetooth/DeviceList.qml | 2 +- modules/controlcenter/launcher/LauncherPane.qml | 2 +- modules/controlcenter/network/NetworkingPane.qml | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index a22ad99..5bec5f8 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -34,7 +34,7 @@ ColumnLayout { StyledText { text: root.title - font.pointSize: Appearance.font.size.normal + font.pointSize: Appearance.font.size.larger font.weight: 500 } diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 502134a..cf6e85a 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -48,7 +48,7 @@ RowLayout { StyledText { text: qsTr("Output devices (%1)").arg(Audio.sinks.length) - font.pointSize: Appearance.font.size.large + font.pointSize: Appearance.font.size.normal font.weight: 500 } @@ -119,7 +119,7 @@ RowLayout { StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Input devices (%1)").arg(Audio.sources.length) - font.pointSize: Appearance.font.size.large + font.pointSize: Appearance.font.size.normal font.weight: 500 } diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index 3831e4a..8bf5daa 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -97,7 +97,7 @@ ColumnLayout { StyledText { Layout.fillWidth: true text: qsTr("Devices (%1)").arg(Bluetooth.devices.values.length) - font.pointSize: Appearance.font.size.large + font.pointSize: Appearance.font.size.normal font.weight: 500 } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index dd00877..7408101 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -139,7 +139,7 @@ RowLayout { StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Applications (%1)").arg(allAppsDb.apps.length) - font.pointSize: Appearance.font.size.larger + font.pointSize: Appearance.font.size.normal font.weight: 500 } diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 5271c56..7db6f54 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -116,7 +116,7 @@ RowLayout { StyledText { text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) - font.pointSize: Appearance.font.size.large + font.pointSize: Appearance.font.size.normal font.weight: 500 } } @@ -243,7 +243,7 @@ RowLayout { StyledText { text: qsTr("Networks (%1)").arg(Nmcli.networks.length) - font.pointSize: Appearance.font.size.large + font.pointSize: Appearance.font.size.normal font.weight: 500 } -- cgit v1.2.3-freya From 5994c77dea6eec5a70458872872997d3f014cfb6 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 14:11:40 -0500 Subject: controlcenter: minor adjustments of accordion menu --- modules/controlcenter/audio/AudioPane.qml | 255 +++++++++++++---------- modules/controlcenter/network/NetworkingPane.qml | 14 ++ 2 files changed, 156 insertions(+), 113 deletions(-) diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index cf6e85a..616d479 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -24,166 +24,195 @@ RowLayout { Layout.minimumWidth: 420 Layout.fillHeight: true - ColumnLayout { + StyledFlickable { 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 + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContent.height - spacing: Appearance.spacing.small + ColumnLayout { + id: leftContent - RowLayout { - spacing: Appearance.spacing.smaller + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal - StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 + // Settings header above the collapsible sections + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } } - Item { + CollapsibleSection { + id: outputDevicesSection + Layout.fillWidth: true - } - } + title: qsTr("Output devices") + expanded: true - StyledText { - text: qsTr("Output devices (%1)").arg(Audio.sinks.length) - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - StyledText { - text: qsTr("All available output devices") - color: Colours.palette.m3outline - } + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - StyledListView { - id: outputView + StyledText { + text: qsTr("Devices (%1)").arg(Audio.sinks.length) + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + } - Layout.fillWidth: true - Layout.fillHeight: true + StyledText { + Layout.fillWidth: true + text: qsTr("All available output devices") + color: Colours.palette.m3outline + } - model: Audio.sinks - spacing: Appearance.spacing.small / 2 - clip: true + Repeater { + Layout.fillWidth: true + model: Audio.sinks - StyledScrollBar.vertical: StyledScrollBar { - flickable: outputView - } + delegate: StyledRect { + required property var modelData - delegate: StyledRect { - required property var modelData + Layout.fillWidth: true - anchors.left: parent.left - anchors.right: parent.right + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Audio.sink?.id === modelData.id ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: Audio.sink?.id === modelData.id ? 1 : 0 + border.color: Colours.palette.m3primary - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Audio.sink?.id === modelData.id ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: Audio.sink?.id === modelData.id ? 1 : 0 - border.color: Colours.palette.m3primary + StateLayer { + function onClicked(): void { + Audio.setAudioSink(modelData); + } + } - StateLayer { - function onClicked(): void { - Audio.setAudioSink(modelData); - } - } + RowLayout { + id: outputRowLayout - RowLayout { - id: outputRowLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.normal - spacing: Appearance.spacing.normal + MaterialIcon { + text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group" + font.pointSize: Appearance.font.size.large + fill: Audio.sink?.id === modelData.id ? 1 : 0 + } - MaterialIcon { - text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group" - font.pointSize: Appearance.font.size.large - fill: Audio.sink?.id === modelData.id ? 1 : 0 - } + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 - StyledText { - Layout.fillWidth: true + text: modelData.description || qsTr("Unknown") + font.weight: Audio.sink?.id === modelData.id ? 500 : 400 + } + } - text: modelData.description || qsTr("Unknown") - font.weight: Audio.sink?.id === modelData.id ? 500 : 400 + implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 + } } } - - implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 } - } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Input devices (%1)").arg(Audio.sources.length) - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } + CollapsibleSection { + id: inputDevicesSection - StyledText { - text: qsTr("All available input devices") - color: Colours.palette.m3outline - } + Layout.fillWidth: true + title: qsTr("Input devices") + expanded: true - StyledListView { - id: inputView + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - Layout.fillWidth: true - Layout.fillHeight: true + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Devices (%1)").arg(Audio.sources.length) + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + } - model: Audio.sources - spacing: Appearance.spacing.small / 2 - clip: true + StyledText { + Layout.fillWidth: true + text: qsTr("All available input devices") + color: Colours.palette.m3outline + } - StyledScrollBar.vertical: StyledScrollBar { - flickable: inputView - } + Repeater { + Layout.fillWidth: true + model: Audio.sources - delegate: StyledRect { - required property var modelData + delegate: StyledRect { + required property var modelData - anchors.left: parent.left - anchors.right: parent.right + Layout.fillWidth: true - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Audio.source?.id === modelData.id ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: Audio.source?.id === modelData.id ? 1 : 0 - border.color: Colours.palette.m3primary + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Audio.source?.id === modelData.id ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: Audio.source?.id === modelData.id ? 1 : 0 + border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - Audio.setAudioSource(modelData); - } - } + StateLayer { + function onClicked(): void { + Audio.setAudioSource(modelData); + } + } - RowLayout { - id: inputRowLayout + RowLayout { + id: inputRowLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - MaterialIcon { - text: Audio.source?.id === modelData.id ? "mic" : "mic_external_on" - font.pointSize: Appearance.font.size.large - fill: Audio.source?.id === modelData.id ? 1 : 0 - } + MaterialIcon { + text: "mic" + font.pointSize: Appearance.font.size.large + fill: Audio.source?.id === modelData.id ? 1 : 0 + } - StyledText { - Layout.fillWidth: true + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + + text: modelData.description || qsTr("Unknown") + font.weight: Audio.source?.id === modelData.id ? 500 : 400 + } + } - text: modelData.description || qsTr("Unknown") - font.weight: Audio.source?.id === modelData.id ? 500 : 400 + implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 + } } } - - implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 7db6f54..c5928de 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -106,6 +106,13 @@ RowLayout { title: qsTr("Ethernet") expanded: true + onToggleRequested: { + if (!expanded) { + // Opening ethernet, close wireless + wirelessListSection.expanded = false; + } + } + ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small @@ -233,6 +240,13 @@ RowLayout { title: qsTr("Wireless") expanded: true + onToggleRequested: { + if (!expanded) { + // Opening wireless, close ethernet + ethernetListSection.expanded = false; + } + } + ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small -- cgit v1.2.3-freya From c3f05b7f2feb680dc62c953ed87f945bfb615fe0 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 14:12:45 -0500 Subject: controlcenter: minor adjustments of accordion menu --- modules/controlcenter/audio/AudioPane.qml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 616d479..732bb89 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -62,6 +62,13 @@ RowLayout { title: qsTr("Output devices") expanded: true + onToggleRequested: { + if (!expanded) { + // Opening output devices, close input devices + inputDevicesSection.expanded = false; + } + } + ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small @@ -142,6 +149,13 @@ RowLayout { title: qsTr("Input devices") expanded: true + onToggleRequested: { + if (!expanded) { + // Opening input devices, close output devices + outputDevicesSection.expanded = false; + } + } + ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small -- cgit v1.2.3-freya From 7c2d49240c8fdd3c5eeb53f03691f681e9cc8eab Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 15:34:22 -0500 Subject: controlcenter: changed labels for networking to network to match codebase --- modules/controlcenter/NavRail.qml | 2 +- modules/controlcenter/Session.qml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 80a1ee3..234f447 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -159,7 +159,7 @@ Item { NavItem { Layout.topMargin: Appearance.spacing.large * 2 icon: "router" - label: "networking" + label: "network" } NavItem { diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index 0aad702..efd360d 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -2,11 +2,11 @@ import Quickshell.Bluetooth import QtQuick QtObject { - readonly property list panes: ["networking", "bluetooth", "audio", "appearance", "taskbar", "launcher"] + readonly property list panes: ["network", "bluetooth", "audio", "appearance", "taskbar", "launcher"] required property var root property bool floating: false - property string active: "networking" + property string active: "network" property int activeIndex: 0 property bool navExpanded: false -- cgit v1.2.3-freya From f472be184a012031d490a3eeaf924d1c54b628a2 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 16:21:22 -0500 Subject: controlcenter: fade-out animation on password input dialog close --- .../network/WirelessPasswordDialog.qml | 51 ++++++++++++---------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index d1ce066..31372e0 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -29,9 +29,9 @@ Item { } property bool isClosing: false - visible: session.network.showPasswordDialog && !isClosing - enabled: visible - focus: visible + visible: session.network.showPasswordDialog || isClosing + enabled: session.network.showPasswordDialog && !isClosing + focus: enabled Keys.onEscapePressed: { closeDialog(); @@ -40,14 +40,10 @@ Item { Rectangle { anchors.fill: parent color: Qt.rgba(0, 0, 0, 0.5) - opacity: root.visible && !root.isClosing ? 1 : 0 + opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 Behavior on opacity { - Anim { - property: "opacity" - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.standardDecel - } + Anim {} } MouseArea { @@ -66,22 +62,35 @@ Item { radius: Appearance.rounding.normal color: Colours.tPalette.m3surface - opacity: root.visible && !root.isClosing ? 1 : 0 - scale: root.visible && !root.isClosing ? 1 : 0.9 + opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 + scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7 Behavior on opacity { - Anim { - property: "opacity" - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.standardDecel - } + Anim {} } Behavior on scale { + Anim {} + } + + ParallelAnimation { + running: root.isClosing + onFinished: { + if (root.isClosing) { + root.session.network.showPasswordDialog = false; + root.isClosing = false; + } + } + Anim { + target: dialog + property: "opacity" + to: 0 + } + Anim { + target: dialog property: "scale" - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.standardDecel + to: 0.7 } } @@ -441,11 +450,5 @@ Item { connectButton.connecting = false; connectButton.text = qsTr("Connect"); connectionMonitor.stop(); - - // Wait for fade-out animation to complete before actually hiding - Qt.callLater(() => { - session.network.showPasswordDialog = false; - isClosing = false; - }, Appearance.anim.durations.normal); } } -- cgit v1.2.3-freya From 9de5e790a075d2f1d74113147bfad72b357d1215 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 16:42:23 -0500 Subject: controlcenter: minor tidying (capitalization and filename) --- .../controlcenter/appearance/AppearancePane.qml | 2 +- modules/controlcenter/audio/AudioPane.qml | 2 +- modules/controlcenter/bluetooth/Settings.qml | 2 +- modules/controlcenter/network/NetworkSettings.qml | 106 +++++++++++++++++++++ modules/controlcenter/network/NetworkingPane.qml | 2 +- .../controlcenter/network/NetworkingSettings.qml | 106 --------------------- modules/controlcenter/taskbar/TaskbarPane.qml | 2 +- 7 files changed, 111 insertions(+), 111 deletions(-) create mode 100644 modules/controlcenter/network/NetworkSettings.qml delete mode 100644 modules/controlcenter/network/NetworkingSettings.qml diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index efd67e9..719e9b3 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -992,7 +992,7 @@ RowLayout { StyledText { Layout.alignment: Qt.AlignHCenter - text: qsTr("Appearance settings") + text: qsTr("Appearance Settings") font.pointSize: Appearance.font.size.large font.bold: true } diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 732bb89..6f5a1f4 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -256,7 +256,7 @@ RowLayout { StyledText { Layout.alignment: Qt.AlignHCenter - text: qsTr("Audio settings") + text: qsTr("Audio Settings") font.pointSize: Appearance.font.size.large font.bold: true } diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index fb493ff..fd33af9 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -26,7 +26,7 @@ ColumnLayout { StyledText { Layout.alignment: Qt.AlignHCenter - text: qsTr("Bluetooth settings") + text: qsTr("Bluetooth Settings") font.pointSize: Appearance.font.size.large font.bold: true } diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml new file mode 100644 index 0000000..75a7660 --- /dev/null +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -0,0 +1,106 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "router" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Network Settings") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Ethernet") + description: qsTr("Ethernet device information") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Total devices") + value: qsTr("%1").arg(Nmcli.ethernetDevices.length) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Connected devices") + value: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Wireless") + description: qsTr("WiFi network settings") + } + + SectionContainer { + ToggleRow { + label: qsTr("WiFi enabled") + checked: Nmcli.wifiEnabled + toggle.onToggled: { + Nmcli.enableWifi(checked); + } + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Current connection") + description: qsTr("Active network connection information") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Network") + value: Nmcli.active ? Nmcli.active.ssid : (Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : qsTr("Not connected")) + } + + PropertyRow { + showTopMargin: true + visible: Nmcli.active !== null + label: qsTr("Signal strength") + value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + visible: Nmcli.active !== null + label: qsTr("Security") + value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + visible: Nmcli.active !== null + label: qsTr("Frequency") + value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A") + } + } +} + diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index c5928de..823c19a 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -478,7 +478,7 @@ RowLayout { flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height - NetworkingSettings { + NetworkSettings { id: settingsInner anchors.left: parent.left diff --git a/modules/controlcenter/network/NetworkingSettings.qml b/modules/controlcenter/network/NetworkingSettings.qml deleted file mode 100644 index 2475fed..0000000 --- a/modules/controlcenter/network/NetworkingSettings.qml +++ /dev/null @@ -1,106 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import qs.components -import qs.components.controls -import qs.components.effects -import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts - -ColumnLayout { - id: root - - required property Session session - - spacing: Appearance.spacing.normal - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "router" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Networking settings") - font.pointSize: Appearance.font.size.large - font.bold: true - } - - SectionHeader { - Layout.topMargin: Appearance.spacing.large - title: qsTr("Ethernet") - description: qsTr("Ethernet device information") - } - - SectionContainer { - contentSpacing: Appearance.spacing.small / 2 - - PropertyRow { - label: qsTr("Total devices") - value: qsTr("%1").arg(Nmcli.ethernetDevices.length) - } - - PropertyRow { - showTopMargin: true - label: qsTr("Connected devices") - value: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) - } - } - - SectionHeader { - Layout.topMargin: Appearance.spacing.large - title: qsTr("Wireless") - description: qsTr("WiFi network settings") - } - - SectionContainer { - ToggleRow { - label: qsTr("WiFi enabled") - checked: Nmcli.wifiEnabled - toggle.onToggled: { - Nmcli.enableWifi(checked); - } - } - } - - SectionHeader { - Layout.topMargin: Appearance.spacing.large - title: qsTr("Current connection") - description: qsTr("Active network connection information") - } - - SectionContainer { - contentSpacing: Appearance.spacing.small / 2 - - PropertyRow { - label: qsTr("Network") - value: Nmcli.active ? Nmcli.active.ssid : (Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : qsTr("Not connected")) - } - - PropertyRow { - showTopMargin: true - visible: Nmcli.active !== null - label: qsTr("Signal strength") - value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A") - } - - PropertyRow { - showTopMargin: true - visible: Nmcli.active !== null - label: qsTr("Security") - value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") - } - - PropertyRow { - showTopMargin: true - visible: Nmcli.active !== null - label: qsTr("Frequency") - value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A") - } - } -} - diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index cf52fd3..0dcc152 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -631,7 +631,7 @@ RowLayout { StyledText { Layout.alignment: Qt.AlignHCenter - text: qsTr("Taskbar settings") + text: qsTr("Taskbar Settings") font.pointSize: Appearance.font.size.large font.bold: true } -- cgit v1.2.3-freya From 45ef91998e4586dbf16c6ea3db0a9f4e19f4487e Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 17:42:49 -0500 Subject: tray: wireless password input popout --- modules/bar/popouts/Content.qml | 48 ++ modules/bar/popouts/Network.qml | 46 +- modules/bar/popouts/WirelessPasswordPopout.qml | 599 +++++++++++++++++++++++++ modules/bar/popouts/Wrapper.qml | 28 +- 4 files changed, 719 insertions(+), 2 deletions(-) create mode 100644 modules/bar/popouts/WirelessPasswordPopout.qml diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 661a41c..7561eec 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -32,8 +32,10 @@ Item { } Popout { + id: networkPopout name: "network" sourceComponent: Network { + wrapper: root.wrapper view: "wireless" } } @@ -41,10 +43,56 @@ Item { Popout { name: "ethernet" sourceComponent: Network { + wrapper: root.wrapper view: "ethernet" } } + Popout { + id: passwordPopout + name: "wirelesspassword" + sourceComponent: WirelessPasswordPopout { + id: passwordComponent + wrapper: root.wrapper + network: networkPopout.item?.passwordNetwork ?? null + } + + Connections { + target: root.wrapper + function onCurrentNameChanged() { + // Update network immediately when password popout becomes active + if (root.wrapper.currentName === "wirelesspassword") { + // Set network immediately if available + if (networkPopout.item && networkPopout.item.passwordNetwork) { + if (passwordPopout.item) { + passwordPopout.item.network = networkPopout.item.passwordNetwork; + } + } + // Also try after a short delay in case networkPopout.item wasn't ready + Qt.callLater(() => { + if (passwordPopout.item && networkPopout.item && networkPopout.item.passwordNetwork) { + passwordPopout.item.network = networkPopout.item.passwordNetwork; + } + }, 100); + } + } + } + + Connections { + target: networkPopout + function onItemChanged() { + // When network popout loads, update password popout if it's active + if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) { + Qt.callLater(() => { + if (networkPopout.item && networkPopout.item.passwordNetwork) { + passwordPopout.item.network = networkPopout.item.passwordNetwork; + } + }); + } + } + } + } + Popout { name: "bluetooth" sourceComponent: Bluetooth { diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index b807ce4..93ff867 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -12,8 +12,12 @@ import QtQuick.Layouts ColumnLayout { id: root + required property Item wrapper + property string connectingToSsid: "" property string view: "wireless" // "wireless" or "ethernet" + property var passwordNetwork: null + property bool showPasswordDialog: false spacing: Appearance.spacing.small width: Config.bar.sizes.networkWidth @@ -129,7 +133,27 @@ ColumnLayout { Nmcli.disconnectFromNetwork(); } else { root.connectingToSsid = networkItem.modelData.ssid; - Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); + // Check if network is secure + if (networkItem.modelData.isSecure) { + // Try to connect first - will show password dialog if password is needed + Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, (result) => { + if (result && result.needsPassword) { + // Password is required - show password dialog + root.passwordNetwork = networkItem.modelData; + root.showPasswordDialog = true; + root.wrapper.currentName = "wirelesspassword"; + } else if (result && result.success) { + // Connection successful with saved password + root.connectingToSsid = ""; + } else { + // Connection failed for other reasons + root.connectingToSsid = ""; + } + }); + } else { + // Open network, no password needed + Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); + } } } } @@ -329,6 +353,15 @@ ColumnLayout { function onActiveChanged(): void { if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { root.connectingToSsid = ""; + // Close password dialog if we successfully connected + if (root.showPasswordDialog && root.passwordNetwork && + Nmcli.active.ssid === root.passwordNetwork.ssid) { + root.showPasswordDialog = false; + root.passwordNetwork = null; + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + } } } @@ -338,6 +371,17 @@ ColumnLayout { } } + Connections { + target: root.wrapper + function onCurrentNameChanged(): void { + // Clear password network when leaving password dialog + if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) { + root.showPasswordDialog = false; + root.passwordNetwork = null; + } + } + } + component Toggle: RowLayout { required property string label property alias checked: toggle.checked diff --git a/modules/bar/popouts/WirelessPasswordPopout.qml b/modules/bar/popouts/WirelessPasswordPopout.qml new file mode 100644 index 0000000..80bdd10 --- /dev/null +++ b/modules/bar/popouts/WirelessPasswordPopout.qml @@ -0,0 +1,599 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.controls +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Item wrapper + property var network: null + property bool isClosing: false + + readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" + + Connections { + target: root.wrapper + function onCurrentNameChanged() { + if (root.wrapper.currentName === "wirelesspassword") { + // Update network when popout becomes active + Qt.callLater(() => { + // Try to get network from parent Content's networkPopout + const content = root.parent?.parent?.parent; + if (content) { + const networkPopout = content.children.find(c => c.name === "network"); + if (networkPopout && networkPopout.item) { + root.network = networkPopout.item.passwordNetwork; + } + } + // Force focus to password container when popout becomes active + Qt.callLater(() => { + passwordContainer.forceActiveFocus(); + }, 100); + }, 100); + } + } + } + + spacing: Appearance.spacing.normal + + implicitWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + + visible: shouldBeVisible || isClosing + enabled: shouldBeVisible && !isClosing + focus: enabled + + Component.onCompleted: { + if (shouldBeVisible) { + Qt.callLater(() => { + root.forceActiveFocus(); + passwordContainer.forceActiveFocus(); + }, 150); + } + } + + onShouldBeVisibleChanged: { + if (shouldBeVisible) { + Qt.callLater(() => { + root.forceActiveFocus(); + passwordContainer.forceActiveFocus(); + }, 150); + } + } + + Keys.onEscapePressed: { + closeDialog(); + } + + StyledRect { + Layout.fillWidth: true + Layout.preferredWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surface + opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 + scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + ParallelAnimation { + running: root.isClosing + onFinished: { + if (root.isClosing) { + root.isClosing = false; + } + } + + Anim { + target: parent + property: "opacity" + to: 0 + } + Anim { + target: parent + property: "scale" + to: 0.7 + } + } + + Keys.onEscapePressed: closeDialog(); + + ColumnLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "lock" + font.pointSize: Appearance.font.size.extraLarge * 2 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enter password") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + id: networkNameText + Layout.alignment: Qt.AlignHCenter + text: { + if (root.network) { + const ssid = root.network.ssid; + if (ssid && ssid.length > 0) { + return qsTr("Network: %1").arg(ssid); + } + } + return qsTr("Network: Unknown"); + } + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + Timer { + interval: 50 + running: root.shouldBeVisible && (!root.network || !root.network.ssid) + repeat: true + property int attempts: 0 + onTriggered: { + attempts++; + // Keep trying to get network from Network component + const content = root.parent?.parent?.parent; + if (content) { + const networkPopout = content.children.find(c => c.name === "network"); + if (networkPopout && networkPopout.item && networkPopout.item.passwordNetwork) { + root.network = networkPopout.item.passwordNetwork; + } + } + // Stop if we got it or after 20 attempts (1 second) + if ((root.network && root.network.ssid) || attempts >= 20) { + stop(); + attempts = 0; + } + } + onRunningChanged: { + if (!running) { + attempts = 0; + } + } + } + + StyledText { + id: statusText + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.spacing.small + visible: connectButton.connecting + text: { + if (connectButton.connecting) { + return qsTr("Connecting..."); + } + return ""; + } + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + font.weight: 400 + wrapMode: Text.WordWrap + Layout.maximumWidth: parent.width - Appearance.padding.large * 2 + } + + FocusScope { + id: passwordContainer + objectName: "passwordContainer" + Layout.topMargin: Appearance.spacing.large + Layout.fillWidth: true + implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + + focus: true + activeFocusOnTab: true + Keys.onPressed: event => { + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { + if (connectButton.enabled) { + connectButton.clicked(); + } + event.accepted = true; + } else if (event.key === Qt.Key_Backspace) { + if (event.modifiers & Qt.ControlModifier) { + passwordBuffer = ""; + } else { + passwordBuffer = passwordBuffer.slice(0, -1); + } + event.accepted = true; + } else if (event.text && event.text.length > 0) { + passwordBuffer += event.text; + event.accepted = true; + } + } + + property string passwordBuffer: "" + + Connections { + target: root + function onShouldBeVisibleChanged(): void { + if (root.shouldBeVisible) { + Qt.callLater(() => { + passwordContainer.forceActiveFocus(); + }, 50); + passwordContainer.passwordBuffer = ""; + } + } + } + + Component.onCompleted: { + if (root.shouldBeVisible) { + Qt.callLater(() => { + passwordContainer.forceActiveFocus(); + }, 100); + } + } + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + border.width: passwordContainer.activeFocus ? 2 : 1 + border.color: passwordContainer.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline + + Behavior on border.color { + CAnim {} + } + } + + StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor + + function onClicked(): void { + passwordContainer.forceActiveFocus(); + } + } + + StyledText { + id: placeholder + + anchors.centerIn: parent + text: qsTr("Password") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + font.family: Appearance.font.family.mono + opacity: passwordContainer.passwordBuffer ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + + ListView { + id: charList + + readonly property int fullWidth: count * (implicitHeight + spacing) - spacing + + anchors.centerIn: parent + implicitWidth: fullWidth + implicitHeight: Appearance.font.size.normal + + orientation: Qt.Horizontal + spacing: Appearance.spacing.small / 2 + interactive: false + + model: ScriptModel { + values: passwordContainer.passwordBuffer.split("") + } + + delegate: StyledRect { + id: ch + + implicitWidth: implicitHeight + implicitHeight: charList.implicitHeight + + color: Colours.palette.m3onSurface + radius: Appearance.rounding.small / 2 + + opacity: 0 + scale: 0 + Component.onCompleted: { + opacity = 1; + scale = 1; + } + ListView.onRemove: removeAnim.start() + + SequentialAnimation { + id: removeAnim + + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: true + } + ParallelAnimation { + Anim { + target: ch + property: "opacity" + to: 0 + } + Anim { + target: ch + property: "scale" + to: 0.5 + } + } + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: false + } + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } + + Behavior on implicitWidth { + Anim {} + } + } + } + + RowLayout { + Layout.topMargin: Appearance.spacing.normal + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + SimpleButton { + id: cancelButton + + Layout.fillWidth: true + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + color: Colours.palette.m3secondaryContainer + onColor: Colours.palette.m3onSecondaryContainer + text: qsTr("Cancel") + + onClicked: closeDialog(); + } + + SimpleButton { + id: connectButton + + Layout.fillWidth: true + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + color: Colours.palette.m3primary + onColor: Colours.palette.m3onPrimary + text: qsTr("Connect") + enabled: passwordContainer.passwordBuffer.length > 0 && !connecting + + property bool connecting: false + + onClicked: { + if (!root.network || connecting) { + return; + } + + const password = passwordContainer.passwordBuffer; + if (!password || password.length === 0) { + return; + } + + // Set connecting state + connecting = true; + enabled = false; + text = qsTr("Connecting..."); + + // Connect to network + Nmcli.connectToNetwork( + root.network.ssid, + password, + root.network.bssid || "", + (result) => { + if (result && result.success) { + // Connection successful, monitor will handle the rest + } else if (result && result.needsPassword) { + // Shouldn't happen since we provided password + connectionMonitor.stop(); + connecting = false; + enabled = true; + text = qsTr("Connect"); + } else { + // Connection failed immediately - return to network popout + connectionMonitor.stop(); + connecting = false; + enabled = true; + text = qsTr("Connect"); + Qt.callLater(() => { + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + }, 500); + } + } + ); + + // Start monitoring connection + connectionMonitor.start(); + } + } + } + } + } + + function checkConnectionStatus(): void { + if (!root.shouldBeVisible || !connectButton.connecting) { + return; + } + + // Check if we're connected to the target network (case-insensitive SSID comparison) + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && + Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + + if (isConnected) { + // Successfully connected - give it a moment for network list to update + Qt.callLater(() => { + // Double-check connection is still active + if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { + const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + // Return to network popout on successful connection + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + closeDialog(); + } + } + }, 500); + return; + } + + // Check for connection failures - if pending connection was cleared but we're not connected + if (Nmcli.pendingConnection === null && connectButton.connecting) { + // Wait a bit more before giving up (allow time for connection to establish) + if (connectionMonitor.repeatCount > 10) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + // Return to network popout on timeout failure + Qt.callLater(() => { + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + }, 500); + } + } + } + + Timer { + id: connectionMonitor + interval: 1000 + repeat: true + triggeredOnStart: false + property int repeatCount: 0 + + onTriggered: { + repeatCount++; + checkConnectionStatus(); + } + + onRunningChanged: { + if (!running) { + repeatCount = 0; + } + } + } + + Connections { + target: Nmcli + function onActiveChanged() { + if (root.shouldBeVisible) { + checkConnectionStatus(); + } + } + function onConnectionFailed(ssid: string) { + if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + // Return to network popout on connection failure + Qt.callLater(() => { + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + }, 500); + } + } + } + + function closeDialog(): void { + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); + + // Return to network popout + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + } + + component SimpleButton: StyledRect { + id: button + + property color onColor: Colours.palette.m3onSurface + property alias disabled: stateLayer.disabled + property alias text: label.text + property alias enabled: stateLayer.enabled + property string icon: "" + + implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: rowLayout.implicitHeight + Appearance.padding.small + radius: Appearance.rounding.normal + + StateLayer { + id: stateLayer + color: parent.onColor + function onClicked(): void { + if (parent.enabled !== false) { + parent.clicked(); + } + } + } + + RowLayout { + id: rowLayout + anchors.centerIn: parent + spacing: Appearance.spacing.small + + MaterialIcon { + id: iconItem + visible: button.icon.length > 0 + text: button.icon + color: button.onColor + font.pointSize: Appearance.font.size.large + } + + StyledText { + id: label + Layout.leftMargin: button.icon.length > 0 ? Appearance.padding.smaller : 0 + color: parent.parent.onColor + } + } + + signal clicked + } +} + diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 4be47e4..5ef4f9d 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -55,7 +55,25 @@ Item { implicitWidth: nonAnimWidth implicitHeight: nonAnimHeight - Keys.onEscapePressed: close() + focus: hasCurrent + Keys.onEscapePressed: { + // Forward escape to password popout if active, otherwise close + if (currentName === "wirelesspassword" && content.item) { + const passwordPopout = content.item.children.find(c => c.name === "wirelesspassword"); + if (passwordPopout && passwordPopout.item) { + passwordPopout.item.closeDialog(); + return; + } + } + close(); + } + + Keys.onPressed: event => { + // Don't intercept keys when password popout is active - let it handle them + if (currentName === "wirelesspassword") { + event.accepted = false; + } + } HyprlandFocusGrab { active: root.isDetached @@ -70,6 +88,14 @@ Item { property: "WlrLayershell.keyboardFocus" value: WlrKeyboardFocus.OnDemand } + + Binding { + when: root.hasCurrent && root.currentName === "wirelesspassword" + + target: QsWindow.window + property: "WlrLayershell.keyboardFocus" + value: WlrKeyboardFocus.OnDemand + } Comp { id: content -- cgit v1.2.3-freya From b40b797caf7fb1c2aa66502791aedd4ef61cbed2 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 17:44:41 -0500 Subject: controlcenter: connected wireless network at top of list --- modules/controlcenter/network/WirelessList.qml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index f4e76b4..00af47a 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -94,7 +94,15 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - model: Nmcli.networks + model: ScriptModel { + values: [...Nmcli.networks].sort((a, b) => { + // Put active/connected network first + if (a.active !== b.active) + return b.active - a.active; + // Then sort by signal strength + return b.strength - a.strength; + }) + } spacing: Appearance.spacing.small / 2 clip: true -- cgit v1.2.3-freya From 9a08d6666eeef12608a834bdd54147d5075c10cd Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 18:00:24 -0500 Subject: controlcenter: auto-focus reliably for wireless password input --- .../controlcenter/network/WirelessPasswordDialog.qml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 31372e0..d0c023f 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -170,12 +170,27 @@ Item { property string passwordBuffer: "" + Connections { + target: root.session.network + function onShowPasswordDialogChanged(): void { + if (root.session.network.showPasswordDialog) { + // Use callLater to ensure focus happens after dialog is fully rendered + Qt.callLater(() => { + passwordContainer.forceActiveFocus(); + passwordContainer.passwordBuffer = ""; + }); + } + } + } + Connections { target: root function onVisibleChanged(): void { if (root.visible) { - passwordContainer.forceActiveFocus(); - passwordContainer.passwordBuffer = ""; + // Use callLater to ensure focus happens after dialog is fully rendered + Qt.callLater(() => { + passwordContainer.forceActiveFocus(); + }); } } } -- cgit v1.2.3-freya From bd6122eaef4efd74865000bcf94fd9cb8ac84b44 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 22:43:09 -0500 Subject: controlcenter connected network top of list fix --- modules/controlcenter/network/NetworkingPane.qml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 823c19a..a44a54c 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -9,6 +9,7 @@ import qs.components.effects import qs.components.containers import qs.services import qs.config +import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Layouts @@ -279,7 +280,15 @@ RowLayout { id: wirelessRepeater Layout.fillWidth: true - model: Nmcli.networks + model: ScriptModel { + values: [...Nmcli.networks].sort((a, b) => { + // Put active/connected network first + if (a.active !== b.active) + return b.active - a.active; + // Then sort by signal strength + return b.strength - a.strength; + }) + } delegate: StyledRect { required property var modelData -- cgit v1.2.3-freya From d123dc0b7ef242baa6d9ee96804003a5855899a8 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 22:50:03 -0500 Subject: controlcenter: search applications in launcher pane --- modules/controlcenter/launcher/LauncherPane.qml | 152 +++++++++++++++++++++++- 1 file changed, 149 insertions(+), 3 deletions(-) diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 7408101..d0bd406 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -15,6 +15,7 @@ import Quickshell.Io import Quickshell.Widgets import QtQuick import QtQuick.Layouts +import "../../../utils/scripts/fuzzysort.js" as Fuzzy RowLayout { id: root @@ -109,6 +110,68 @@ RowLayout { entries: DesktopEntries.applications.values // No filter - show all apps } + property string searchText: "" + + function filterApps(search: string): list { + // If search is empty, return all apps directly + if (!search || search.trim() === "") { + // Convert QQmlListProperty to array + const apps = []; + for (let i = 0; i < allAppsDb.apps.length; i++) { + apps.push(allAppsDb.apps[i]); + } + return apps; + } + + if (!allAppsDb.apps || allAppsDb.apps.length === 0) { + return []; + } + + // Prepare apps for fuzzy search + const preparedApps = []; + for (let i = 0; i < allAppsDb.apps.length; i++) { + const app = allAppsDb.apps[i]; + const name = app.name || app.entry?.name || ""; + preparedApps.push({ + _item: app, + name: Fuzzy.prepare(name) + }); + } + + // Perform fuzzy search + const results = Fuzzy.go(search, preparedApps, { + all: true, + keys: ["name"], + scoreFn: r => r[0].score + }); + + // Return sorted by score (highest first) + return results + .sort((a, b) => b._score - a._score) + .map(r => r.obj._item); + } + + property list filteredApps: [] + + function updateFilteredApps() { + filteredApps = filterApps(searchText); + } + + onSearchTextChanged: { + updateFilteredApps(); + } + + Component.onCompleted: { + updateFilteredApps(); + } + + Connections { + target: allAppsDb + function onAppsChanged() { + updateFilteredApps(); + } + } + Item { Layout.preferredWidth: Math.floor(parent.width * 0.4) Layout.minimumWidth: 420 @@ -138,7 +201,7 @@ RowLayout { StyledText { Layout.topMargin: Appearance.spacing.large - text: qsTr("Applications (%1)").arg(allAppsDb.apps.length) + text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length) font.pointSize: Appearance.font.size.normal font.weight: 500 } @@ -148,12 +211,95 @@ RowLayout { color: Colours.palette.m3outline } + StyledRect { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + Layout.bottomMargin: Appearance.spacing.small + + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.full + + implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight) + + MaterialIcon { + id: searchIcon + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: Appearance.padding.normal + + text: "search" + color: Colours.palette.m3onSurfaceVariant + } + + StyledTextField { + id: searchField + + anchors.left: searchIcon.right + anchors.right: clearIcon.left + anchors.leftMargin: Appearance.spacing.small + anchors.rightMargin: Appearance.spacing.small + + topPadding: Appearance.padding.normal + bottomPadding: Appearance.padding.normal + + placeholderText: qsTr("Search applications...") + + onTextChanged: { + root.searchText = text; + } + } + + MaterialIcon { + id: clearIcon + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: Appearance.padding.normal + + width: searchField.text ? implicitWidth : implicitWidth / 2 + opacity: { + if (!searchField.text) + return 0; + if (clearMouse.pressed) + return 0.7; + if (clearMouse.containsMouse) + return 0.8; + return 1; + } + + text: "close" + color: Colours.palette.m3onSurfaceVariant + + MouseArea { + id: clearMouse + + anchors.fill: parent + hoverEnabled: true + cursorShape: searchField.text ? Qt.PointingHandCursor : undefined + + onClicked: searchField.text = "" + } + + Behavior on width { + Anim { + duration: Appearance.anim.durations.small + } + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.small + } + } + } + } + StyledListView { Layout.fillWidth: true Layout.fillHeight: true - Layout.topMargin: Appearance.spacing.normal - model: allAppsDb.apps + model: root.filteredApps spacing: Appearance.spacing.small / 2 clip: true -- cgit v1.2.3-freya From 9825ad4d3102130ec40bb9324c3e37e1622c9c57 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:53:30 +1100 Subject: nmcli: fix errors + disable most logs --- services/Nmcli.qml | 624 ++++++++++++++++++++++++++++------------------------- 1 file changed, 333 insertions(+), 291 deletions(-) diff --git a/services/Nmcli.qml b/services/Nmcli.qml index a9f9e8e..24a93da 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -1,4 +1,5 @@ pragma Singleton +pragma ComponentBehavior: Bound import Quickshell import Quickshell.Io @@ -19,7 +20,7 @@ Singleton { readonly property AccessPoint active: networks.find(n => n.active) ?? null property list savedConnections: [] property list savedConnectionSsids: [] - + property var wifiConnectionQueue: [] property int currentSsidQueryIndex: 0 property var pendingConnection: null @@ -28,9 +29,8 @@ Singleton { property var ethernetDeviceDetails: null property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null - + property list activeProcesses: [] - property var debugLogger: null // Constants readonly property string deviceTypeWifi: "wifi" @@ -55,63 +55,35 @@ Singleton { readonly property string connectionParamPassword: "password" readonly property string connectionParamBssid: "802-11-wireless.bssid" - function setDebugLogger(logger: var): void { - root.debugLogger = logger; - } - - function log(message: string): void { - if (root.debugLogger) { - root.debugLogger(message); - } else { - console.log("[Nmcli]", message); - } - } - - function appendLog(message: string): void { - log(message); - } - function detectPasswordRequired(error: string): bool { if (!error || error.length === 0) { return false; } - - return (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); + + return (error.includes("Secrets were required") || error.includes("Secrets were required, but not provided") || error.includes("No secrets provided") || error.includes("802-11-wireless-security.psk") || error.includes("password for") || (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && !error.includes("Connection activated") && !error.includes("successfully"); } function parseNetworkOutput(output: string): list { if (!output || output.length === 0) { return []; } - + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; const rep = new RegExp("\\\\:", "g"); const rep2 = new RegExp(PLACEHOLDER, "g"); - - const allNetworks = output.trim().split("\n") - .filter(line => line && line.length > 0) - .map(n => { - const net = n.replace(rep, PLACEHOLDER).split(":"); - return { - active: net[0] === "yes", - strength: parseInt(net[1] || "0", 10) || 0, - frequency: parseInt(net[2] || "0", 10) || 0, - ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), - bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), - security: (net[5] ?? "").trim() - }; - }) - .filter(n => n.ssid && n.ssid.length > 0); - + + const allNetworks = output.trim().split("\n").filter(line => line && line.length > 0).map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1] || "0", 10) || 0, + frequency: parseInt(net[2] || "0", 10) || 0, + ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), + bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), + security: (net[5] ?? "").trim() + }; + }).filter(n => n.ssid && n.ssid.length > 0); + return allNetworks; } @@ -119,7 +91,7 @@ Singleton { if (!networks || networks.length === 0) { return []; } - + const networkMap = new Map(); for (const network of networks) { const existing = networkMap.get(network.ssid); @@ -135,7 +107,7 @@ Singleton { } } } - + return Array.from(networkMap.values()); } @@ -143,7 +115,7 @@ Singleton { if (!command || command.length === 0) { return false; } - + return command.includes(root.nmcliCommandWifi) || command.includes(root.nmcliCommandConnection); } @@ -151,16 +123,16 @@ Singleton { if (!output || output.length === 0) { return []; } - + const interfaces = []; const lines = output.trim().split("\n"); - + for (const line of lines) { const parts = line.split(":"); if (parts.length >= 2) { const deviceType = parts[1]; let shouldInclude = false; - + if (filterType === root.deviceTypeWifi && deviceType === root.deviceTypeWifi) { shouldInclude = true; } else if (filterType === root.deviceTypeEthernet && deviceType === root.deviceTypeEthernet) { @@ -168,7 +140,7 @@ Singleton { } else if (filterType === "both" && (deviceType === root.deviceTypeWifi || deviceType === root.deviceTypeEthernet)) { shouldInclude = true; } - + if (shouldInclude) { interfaces.push({ device: parts[0] || "", @@ -179,7 +151,7 @@ Singleton { } } } - + return interfaces; } @@ -187,53 +159,53 @@ Singleton { if (!state || state.length === 0) { return false; } - - return state === "100 (connected)" || - state === "connected" || - state.startsWith("connected"); + + return state === "100 (connected)" || state === "connected" || state.startsWith("connected"); } function executeCommand(args: list, callback: var): void { const proc = commandProc.createObject(root); proc.command = ["nmcli", ...args]; proc.callback = callback; - + activeProcesses.push(proc); - + proc.processFinished.connect(() => { const index = activeProcesses.indexOf(proc); if (index >= 0) { activeProcesses.splice(index, 1); } }); - + Qt.callLater(() => { proc.exec(proc.command); }); } function getDeviceStatus(callback: var): void { - executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { - if (callback) callback(result.output); + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { + if (callback) + callback(result.output); }); } function getWirelessInterfaces(callback: var): void { - executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeWifi); root.wirelessInterfaces = interfaces; - if (callback) callback(interfaces); + if (callback) + callback(interfaces); }); } function getEthernetInterfaces(callback: var): void { - executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeEthernet); const devices = []; - + for (const iface of interfaces) { const connected = isConnectedState(iface.state); - + devices.push({ interface: iface.device, type: iface.type, @@ -248,16 +220,17 @@ Singleton { speed: "" }); } - + root.ethernetInterfaces = interfaces; root.ethernetDevices = devices; - if (callback) callback(interfaces); + if (callback) + callback(interfaces); }); } function connectEthernet(connectionName: string, interfaceName: string, callback: var): void { if (connectionName && connectionName.length > 0) { - executeCommand([root.nmcliCommandConnection, "up", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "up", connectionName], result => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); @@ -268,10 +241,11 @@ Singleton { } }, 500); } - if (callback) callback(result); + if (callback) + callback(result); }); } else if (interfaceName && interfaceName.length > 0) { - executeCommand([root.nmcliCommandDevice, "connect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], result => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); @@ -280,62 +254,94 @@ Singleton { }, 1000); }, 500); } - if (callback) callback(result); + if (callback) + callback(result); }); } else { - if (callback) callback({ success: false, output: "", error: "No connection name or interface specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No connection name or interface specified", + exitCode: -1 + }); } } function disconnectEthernet(connectionName: string, callback: var): void { if (!connectionName || connectionName.length === 0) { - if (callback) callback({ success: false, output: "", error: "No connection name specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No connection name specified", + exitCode: -1 + }); return; } - - executeCommand([root.nmcliCommandConnection, "down", connectionName], (result) => { + + executeCommand([root.nmcliCommandConnection, "down", connectionName], result => { if (result.success) { root.ethernetDeviceDetails = null; Qt.callLater(() => { getEthernetInterfaces(() => {}); }, 500); } - if (callback) callback(result); + if (callback) + callback(result); }); } function getAllInterfaces(callback: var): void { - executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { const interfaces = parseDeviceStatusOutput(result.output, "both"); - if (callback) callback(interfaces); + if (callback) + callback(interfaces); }); } function isInterfaceConnected(interfaceName: string, callback: var): void { - executeCommand([root.nmcliCommandDevice, "status"], (result) => { + executeCommand([root.nmcliCommandDevice, "status"], result => { const lines = result.output.trim().split("\n"); for (const line of lines) { const parts = line.split(/\s+/); if (parts.length >= 3 && parts[0] === interfaceName) { const connected = isConnectedState(parts[2]); - if (callback) callback(connected); + if (callback) + callback(connected); return; } } - if (callback) callback(false); + if (callback) + callback(false); }); } function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { if (isSecure) { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; - connectWireless(ssid, "", bssid, (result) => { + connectWireless(ssid, "", bssid, result => { if (result.success) { - if (callback) callback({ success: true, usedSavedPassword: true, output: result.output, error: "", exitCode: 0 }); + if (callback) + callback({ + success: true, + usedSavedPassword: true, + output: result.output, + error: "", + exitCode: 0 + }); } else if (result.needsPassword) { - if (callback) callback({ success: false, needsPassword: true, output: result.output, error: result.error, exitCode: result.exitCode }); + if (callback) + callback({ + success: false, + needsPassword: true, + output: result.output, + error: result.error, + exitCode: result.exitCode + }); } else { - if (callback) callback(result); + if (callback) + callback(result); } }); } else { @@ -351,72 +357,68 @@ Singleton { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; const retries = retryCount !== undefined ? retryCount : 0; const maxRetries = 2; - + if (callback) { - root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback, retryCount: retries }; + root.pendingConnection = { + ssid: ssid, + bssid: hasBssid ? bssid : "", + callback: callback, + retryCount: retries + }; connectionCheckTimer.start(); immediateCheckTimer.checkCount = 0; immediateCheckTimer.start(); } - + if (password && password.length > 0 && hasBssid) { const bssidUpper = bssid.toUpperCase(); createConnectionWithPassword(ssid, bssidUpper, password, callback); return; } - + let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid]; if (password && password.length > 0) { cmd.push(root.connectionParamPassword, password); } - executeCommand(cmd, (result) => { + executeCommand(cmd, result => { if (result.needsPassword && callback) { - if (callback) callback(result); + if (callback) + callback(result); return; } - + if (!result.success && root.pendingConnection && retries < maxRetries) { - log("Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); + console.warn("[NMCLI] Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); Qt.callLater(() => { connectWireless(ssid, password, bssid, callback, retries + 1); }, 1000); - } else if (!result.success && root.pendingConnection) { - } else if (result.success && callback) { - } else if (!result.success && !root.pendingConnection) { - if (callback) callback(result); + } else if (!result.success && root.pendingConnection) {} else if (result.success && callback) {} else if (!result.success && !root.pendingConnection) { + if (callback) + callback(result); } }); } function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void { checkAndDeleteConnection(ssid, () => { - const cmd = [root.nmcliCommandConnection, "add", - root.connectionParamType, root.deviceTypeWifi, - root.connectionParamConName, ssid, - root.connectionParamIfname, "*", - root.connectionParamSsid, ssid, - root.connectionParamBssid, bssidUpper, - root.securityKeyMgmt, root.keyMgmtWpaPsk, - root.securityPsk, password]; - - executeCommand(cmd, (result) => { + const cmd = [root.nmcliCommandConnection, "add", root.connectionParamType, root.deviceTypeWifi, root.connectionParamConName, ssid, root.connectionParamIfname, "*", root.connectionParamSsid, ssid, root.connectionParamBssid, bssidUpper, root.securityKeyMgmt, root.keyMgmtWpaPsk, root.securityPsk, password]; + + executeCommand(cmd, result => { if (result.success) { loadSavedConnections(() => {}); activateConnection(ssid, callback); } else { - const hasDuplicateWarning = result.error && ( - result.error.includes("another connection with the name") || - result.error.includes("Reference the connection by its uuid") - ); - + const hasDuplicateWarning = result.error && (result.error.includes("another connection with the name") || result.error.includes("Reference the connection by its uuid")); + if (hasDuplicateWarning || (result.exitCode > 0 && result.exitCode < 10)) { loadSavedConnections(() => {}); activateConnection(ssid, callback); } else { - log("Connection profile creation failed, trying fallback..."); + console.warn("[NMCLI] Connection profile creation failed, trying fallback..."); let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid, root.connectionParamPassword, password]; - executeCommand(fallbackCmd, (fallbackResult) => { - if (callback) callback(fallbackResult); + executeCommand(fallbackCmd, fallbackResult => { + if (callback) + callback(fallbackResult); }); } } @@ -425,34 +427,38 @@ Singleton { } function checkAndDeleteConnection(ssid: string, callback: var): void { - executeCommand([root.nmcliCommandConnection, "show", ssid], (result) => { + executeCommand([root.nmcliCommandConnection, "show", ssid], result => { if (result.success) { - executeCommand([root.nmcliCommandConnection, "delete", ssid], (deleteResult) => { + executeCommand([root.nmcliCommandConnection, "delete", ssid], deleteResult => { Qt.callLater(() => { - if (callback) callback(); + if (callback) + callback(); }, 300); }); } else { - if (callback) callback(); + if (callback) + callback(); } }); } function activateConnection(connectionName: string, callback: var): void { - executeCommand([root.nmcliCommandConnection, "up", connectionName], (result) => { - if (callback) callback(result); + executeCommand([root.nmcliCommandConnection, "up", connectionName], result => { + if (callback) + callback(result); }); } function loadSavedConnections(callback: var): void { - executeCommand(["-t", "-f", root.connectionListFields, root.nmcliCommandConnection, "show"], (result) => { + executeCommand(["-t", "-f", root.connectionListFields, root.nmcliCommandConnection, "show"], result => { if (!result.success) { root.savedConnections = []; root.savedConnectionSsids = []; - if (callback) callback([]); + if (callback) + callback([]); return; } - + parseConnectionList(result.output, callback); }); } @@ -461,22 +467,22 @@ Singleton { const lines = output.trim().split("\n").filter(line => line.length > 0); const wifiConnections = []; const connections = []; - + for (const line of lines) { const parts = line.split(":"); if (parts.length >= 2) { const name = parts[0]; const type = parts[1]; connections.push(name); - + if (type === root.connectionTypeWireless) { wifiConnections.push(name); } } } - + root.savedConnections = connections; - + if (wifiConnections.length > 0) { root.wifiConnectionQueue = wifiConnections; root.currentSsidQueryIndex = 0; @@ -485,7 +491,8 @@ Singleton { } else { root.savedConnectionSsids = []; root.wifiConnectionQueue = []; - if (callback) callback(root.savedConnectionSsids); + if (callback) + callback(root.savedConnectionSsids); } } @@ -493,8 +500,8 @@ Singleton { if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; root.currentSsidQueryIndex++; - - executeCommand(["-t", "-f", root.wirelessSsidField, root.nmcliCommandConnection, "show", connectionName], (result) => { + + executeCommand(["-t", "-f", root.wirelessSsidField, root.nmcliCommandConnection, "show", connectionName], result => { if (result.success) { processSsidOutput(result.output); } @@ -503,7 +510,8 @@ Singleton { } else { root.wifiConnectionQueue = []; root.currentSsidQueryIndex = 0; - if (callback) callback(root.savedConnectionSsids); + if (callback) + callback(root.savedConnectionSsids); } } @@ -530,70 +538,73 @@ Singleton { return false; } const ssidLower = ssid.toLowerCase().trim(); - + if (root.active && root.active.ssid) { const activeSsidLower = root.active.ssid.toLowerCase().trim(); if (activeSsidLower === ssidLower) { return true; } } - - const hasSsid = root.savedConnectionSsids.some(savedSsid => - savedSsid && savedSsid.toLowerCase().trim() === ssidLower - ); - + + const hasSsid = root.savedConnectionSsids.some(savedSsid => savedSsid && savedSsid.toLowerCase().trim() === ssidLower); + if (hasSsid) { return true; } - - const hasConnectionName = root.savedConnections.some(connName => - connName && connName.toLowerCase().trim() === ssidLower - ); - + + const hasConnectionName = root.savedConnections.some(connName => connName && connName.toLowerCase().trim() === ssidLower); + return hasConnectionName; } function forgetNetwork(ssid: string, callback: var): void { if (!ssid || ssid.length === 0) { - if (callback) callback({ success: false, output: "", error: "No SSID specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No SSID specified", + exitCode: -1 + }); return; } - - const connectionName = root.savedConnections.find(conn => - conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() - ) || ssid; - - executeCommand([root.nmcliCommandConnection, "delete", connectionName], (result) => { + + const connectionName = root.savedConnections.find(conn => conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim()) || ssid; + + executeCommand([root.nmcliCommandConnection, "delete", connectionName], result => { if (result.success) { Qt.callLater(() => { loadSavedConnections(() => {}); }, 500); } - if (callback) callback(result); + if (callback) + callback(result); }); } function disconnect(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], (result) => { - if (callback) callback(result.success ? result.output : ""); + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], result => { + if (callback) + callback(result.success ? result.output : ""); }); } else { - executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], (result) => { - if (callback) callback(result.success ? result.output : ""); + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], result => { + if (callback) + callback(result.success ? result.output : ""); }); } } function disconnectFromNetwork(): void { if (active && active.ssid) { - executeCommand([root.nmcliCommandConnection, "down", active.ssid], (result) => { + executeCommand([root.nmcliCommandConnection, "down", active.ssid], result => { if (result.success) { getNetworks(() => {}); } }); } else { - executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], result => { if (result.success) { getNetworks(() => {}); } @@ -602,13 +613,14 @@ Singleton { } function getDeviceDetails(interfaceName: string, callback: var): void { - executeCommand([root.nmcliCommandDevice, "show", interfaceName], (result) => { - if (callback) callback(result.output); + executeCommand([root.nmcliCommandDevice, "show", interfaceName], result => { + if (callback) + callback(result.output); }); } function refreshStatus(callback: var): void { - getDeviceStatus((output) => { + getDeviceStatus(output => { const lines = output.trim().split("\n"); let connected = false; let activeIf = ""; @@ -631,31 +643,48 @@ Singleton { root.activeInterface = activeIf; root.activeConnection = activeConn; - if (callback) callback({ connected, interface: activeIf, connection: activeConn }); + if (callback) + callback({ + connected, + interface: activeIf, + connection: activeConn + }); }); } function bringInterfaceUp(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand([root.nmcliCommandDevice, "connect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], result => { if (callback) { callback(result); } }); } else { - if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No interface specified", + exitCode: -1 + }); } } function bringInterfaceDown(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], result => { if (callback) { callback(result); } }); } else { - if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No interface specified", + exitCode: -1 + }); } } @@ -664,7 +693,7 @@ Singleton { if (interfaceName && interfaceName.length > 0) { cmd.push(root.connectionParamIfname, interfaceName); } - executeCommand(cmd, (result) => { + executeCommand(cmd, result => { if (callback) { callback(result); } @@ -677,14 +706,16 @@ Singleton { function enableWifi(enabled: bool, callback: var): void { const cmd = enabled ? "on" : "off"; - executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, cmd], (result) => { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, cmd], result => { if (result.success) { - getWifiStatus((status) => { + getWifiStatus(status => { root.wifiEnabled = status; - if (callback) callback(result); + if (callback) + callback(result); }); } else { - if (callback) callback(result); + if (callback) + callback(result); } }); } @@ -695,33 +726,32 @@ Singleton { } function getWifiStatus(callback: var): void { - executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi], (result) => { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi], result => { if (result.success) { const enabled = result.output.trim() === "enabled"; root.wifiEnabled = enabled; - if (callback) callback(enabled); + if (callback) + callback(enabled); } else { - if (callback) callback(root.wifiEnabled); + if (callback) + callback(root.wifiEnabled); } }); } function getNetworks(callback: var): void { - executeCommand(["-g", root.networkDetailFields, "d", "w"], (result) => { + executeCommand(["-g", root.networkDetailFields, "d", "w"], result => { if (!result.success) { - if (callback) callback([]); + if (callback) + callback([]); return; } - + const allNetworks = parseNetworkOutput(result.output); const networks = deduplicateNetworks(allNetworks); const rNetworks = root.networks; - - const destroyed = rNetworks.filter(rn => !networks.find(n => - n.frequency === rn.frequency && - n.ssid === rn.ssid && - n.bssid === rn.bssid - )); + + const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid)); for (const network of destroyed) { const index = rNetworks.indexOf(network); if (index >= 0) { @@ -729,13 +759,9 @@ Singleton { network.destroy(); } } - + for (const network of networks) { - const match = rNetworks.find(n => - n.frequency === network.frequency && - n.ssid === network.ssid && - n.bssid === network.bssid - ); + const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid); if (match) { match.lastIpcObject = network; } else { @@ -744,8 +770,9 @@ Singleton { })); } } - - if (callback) callback(root.networks); + + if (callback) + callback(root.networks); checkPendingConnection(); }); } @@ -755,19 +782,21 @@ Singleton { if (interfaceName && interfaceName.length > 0) { cmd.push(root.connectionParamIfname, interfaceName); } - executeCommand(cmd, (result) => { + executeCommand(cmd, result => { if (!result.success) { - if (callback) callback([]); + if (callback) + callback([]); return; } - + const ssids = []; const lines = result.output.trim().split("\n"); const seenSSIDs = new Set(); - + for (const line of lines) { - if (!line || line.length === 0) continue; - + if (!line || line.length === 0) + continue; + const parts = line.split(":"); if (parts.length >= 1) { const ssid = parts[0].trim(); @@ -785,12 +814,13 @@ Singleton { } } } - + ssids.sort((a, b) => { return b.signalValue - a.signalValue; }); - - if (callback) callback(ssids); + + if (callback) + callback(ssids); }); } @@ -798,13 +828,13 @@ Singleton { if (!proc || !error || error.length === 0) { return false; } - + if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) { return false; } - + const needsPassword = detectPasswordRequired(error); - + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); @@ -827,77 +857,79 @@ Singleton { } return true; } - + return false; } component CommandProcess: Process { id: proc + property var callback: null property list command: [] property bool callbackCalled: false property int exitCode: 0 + signal processFinished environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) stdout: StdioCollector { id: stdoutCollector - onStreamFinished: { - } } stderr: StdioCollector { id: stderrCollector + onStreamFinished: { const error = text.trim(); if (error && error.length > 0) { const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; - handlePasswordRequired(proc, error, output, -1); + root.handlePasswordRequired(proc, error, output, -1); } } } - onExited: { - proc.exitCode = exitCode; + onExited: code => { + exitCode = code; + Qt.callLater(() => { - if (proc.callbackCalled) { - proc.processFinished(); + if (callbackCalled) { + processFinished(); return; } - + if (proc.callback) { const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; - const success = proc.exitCode === 0; + const success = exitCode === 0; const cmdIsConnection = isConnectionCommand(proc.command); - - if (handlePasswordRequired(proc, error, output, proc.exitCode)) { - proc.processFinished(); + + if (root.handlePasswordRequired(proc, error, output, exitCode)) { + processFinished(); return; } - - const needsPassword = cmdIsConnection && detectPasswordRequired(error); - + + const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); + if (!success && cmdIsConnection && root.pendingConnection) { const failedSsid = root.pendingConnection.ssid; root.connectionFailed(failedSsid); } - - proc.callbackCalled = true; - proc.callback({ + + callbackCalled = true; + callback({ success: success, output: output, error: error, exitCode: proc.exitCode, needsPassword: needsPassword || false }); - proc.processFinished(); + processFinished(); } else { - proc.processFinished(); + processFinished(); } }); } @@ -905,6 +937,7 @@ Singleton { Component { id: commandProc + CommandProcess {} } @@ -921,16 +954,18 @@ Singleton { Component { id: apComp + AccessPoint {} } Timer { id: connectionCheckTimer + interval: 4000 onTriggered: { if (root.pendingConnection) { const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - + if (!connected && root.pendingConnection.callback) { let foundPasswordError = false; for (let i = 0; i < root.activeProcesses.length; i++) { @@ -938,9 +973,9 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - if (isConnectionCommand(proc.command)) { - const needsPassword = detectPasswordRequired(error); - + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { const pending = root.pendingConnection; root.pendingConnection = null; @@ -967,7 +1002,7 @@ Singleton { } } } - + if (!foundPasswordError) { const pending = root.pendingConnection; const failedSsid = pending.ssid; @@ -975,10 +1010,10 @@ Singleton { immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; root.connectionFailed(failedSsid); - pending.callback({ - success: false, - output: "", - error: "Connection timeout", + pending.callback({ + success: false, + output: "", + error: "Connection timeout", exitCode: -1, needsPassword: false }); @@ -994,22 +1029,29 @@ Singleton { Timer { id: immediateCheckTimer + + property int checkCount: 0 + interval: 500 repeat: true triggeredOnStart: false - property int checkCount: 0 onTriggered: { if (root.pendingConnection) { checkCount++; const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - + if (connected) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; if (root.pendingConnection.callback) { - root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + root.pendingConnection.callback({ + success: true, + output: "Connected", + error: "", + exitCode: 0 + }); } root.pendingConnection = null; } else { @@ -1018,9 +1060,9 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - if (isConnectionCommand(proc.command)) { - const needsPassword = detectPasswordRequired(error); - + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); @@ -1047,7 +1089,7 @@ Singleton { } } } - + if (checkCount >= 6) { immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; @@ -1069,7 +1111,12 @@ Singleton { immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; if (root.pendingConnection.callback) { - root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + root.pendingConnection.callback({ + success: true, + output: "Connected", + error: "", + exitCode: 0 + }); } root.pendingConnection = null; } else { @@ -1086,13 +1133,13 @@ Singleton { if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { return ""; } - + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; const octet1 = (mask >>> 24) & 0xff; const octet2 = (mask >>> 16) & 0xff; const octet3 = (mask >>> 8) & 0xff; const octet4 = mask & 0xff; - + return `${octet1}.${octet2}.${octet3}.${octet4}`; } @@ -1104,21 +1151,24 @@ Singleton { if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; } else { - if (callback) callback(null); + if (callback) + callback(null); return; } } - - executeCommand(["device", "show", interfaceName], (result) => { + + executeCommand(["device", "show", interfaceName], result => { if (!result.success || !result.output) { root.wirelessDeviceDetails = null; - if (callback) callback(null); + if (callback) + callback(null); return; } - + const details = parseDeviceDetails(result.output, false); root.wirelessDeviceDetails = details; - if (callback) callback(details); + if (callback) + callback(details); }); } @@ -1130,21 +1180,24 @@ Singleton { if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; } else { - if (callback) callback(null); + if (callback) + callback(null); return; } } - - executeCommand(["device", "show", interfaceName], (result) => { + + executeCommand(["device", "show", interfaceName], result => { if (!result.success || !result.output) { root.ethernetDeviceDetails = null; - if (callback) callback(null); + if (callback) + callback(null); return; } - + const details = parseDeviceDetails(result.output, true); root.ethernetDeviceDetails = details; - if (callback) callback(details); + if (callback) + callback(details); }); } @@ -1157,20 +1210,20 @@ Singleton { macAddress: "", speed: "" }; - + if (!output || output.length === 0) { return details; } - + const lines = output.trim().split("\n"); - + for (let i = 0; i < lines.length; i++) { const line = lines[i]; const parts = line.split(":"); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(":").trim(); - + if (key.startsWith("IP4.ADDRESS")) { const ipParts = value.split("/"); details.ipAddress = ipParts[0] || ""; @@ -1196,46 +1249,36 @@ Singleton { } } } - + return details; } Process { id: rescanProc + command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] - onExited: { - getNetworks(() => {}); - } + onExited: root.getNetworks() } Process { id: monitorProc + running: true command: ["nmcli", "monitor"] environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) stdout: SplitParser { - onRead: { - log("Connection state change detected, refreshing..."); - root.refreshOnConnectionChange(); - } - } - - onExited: { - log("Monitor process exited, restarting..."); - Qt.callLater(() => { - monitorProc.running = true; - }, 2000); + onRead: root.refreshOnConnectionChange() } + onExited: Qt.callLater(() => monitorProc.running = true, 2000) } function refreshOnConnectionChange(): void { - getNetworks((networks) => { + getNetworks(networks => { const newActive = root.active; - + if (newActive && newActive.active) { Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { @@ -1246,7 +1289,7 @@ Singleton { getWirelessDeviceDetails(activeWireless.device, () => {}); } } - + if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { return isConnectedState(iface.state); @@ -1260,7 +1303,7 @@ Singleton { root.wirelessDeviceDetails = null; root.ethernetDeviceDetails = null; } - + getWirelessInterfaces(() => {}); getEthernetInterfaces(() => { if (root.activeEthernet && root.activeEthernet.connected) { @@ -1277,7 +1320,7 @@ Singleton { getNetworks(() => {}); loadSavedConnections(() => {}); getEthernetInterfaces(() => {}); - + Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { const activeWireless = root.wirelessInterfaces.find(iface => { @@ -1287,7 +1330,7 @@ Singleton { getWirelessDeviceDetails(activeWireless.device, () => {}); } } - + if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { return isConnectedState(iface.state); @@ -1299,4 +1342,3 @@ Singleton { }, 2000); } } - -- cgit v1.2.3-freya From f617c15d88d291bf1dc3e60c403dfcc4254b4842 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 23:53:07 -0500 Subject: tray: wireless password ripple effect fix / focus --- modules/bar/popouts/WirelessPasswordPopout.qml | 29 +++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/modules/bar/popouts/WirelessPasswordPopout.qml b/modules/bar/popouts/WirelessPasswordPopout.qml index 80bdd10..5a072d8 100644 --- a/modules/bar/popouts/WirelessPasswordPopout.qml +++ b/modules/bar/popouts/WirelessPasswordPopout.qml @@ -206,7 +206,15 @@ ColumnLayout { focus: true activeFocusOnTab: true + + property string passwordBuffer: "" + Keys.onPressed: event => { + // Ensure we have focus when receiving keyboard input + if (!activeFocus) { + forceActiveFocus(); + } + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { if (connectButton.enabled) { connectButton.clicked(); @@ -225,8 +233,6 @@ ColumnLayout { } } - property string passwordBuffer: "" - Connections { target: root function onShouldBeVisibleChanged(): void { @@ -250,18 +256,31 @@ ColumnLayout { StyledRect { anchors.fill: parent radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - border.width: passwordContainer.activeFocus ? 2 : 1 - border.color: passwordContainer.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline + color: passwordContainer.activeFocus + ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) + : Colours.tPalette.m3surfaceContainer + border.width: passwordContainer.activeFocus ? 4 : (root.shouldBeVisible ? 1 : 0) + border.color: passwordContainer.activeFocus + ? Colours.palette.m3primary + : (root.shouldBeVisible ? Colours.palette.m3outline : "transparent") Behavior on border.color { CAnim {} } + + Behavior on border.width { + CAnim {} + } + + Behavior on color { + CAnim {} + } } StateLayer { hoverEnabled: false cursorShape: Qt.IBeamCursor + radius: Appearance.rounding.normal function onClicked(): void { passwordContainer.forceActiveFocus(); -- cgit v1.2.3-freya From 1e4e00dd826c5dbe9026d1d8c4d1363e3a50a8b6 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 23:57:28 -0500 Subject: tray: wireless password displays errors and resets --- modules/bar/popouts/WirelessPasswordPopout.qml | 73 +++++++++++++++++--------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/modules/bar/popouts/WirelessPasswordPopout.qml b/modules/bar/popouts/WirelessPasswordPopout.qml index 5a072d8..8bd256e 100644 --- a/modules/bar/popouts/WirelessPasswordPopout.qml +++ b/modules/bar/popouts/WirelessPasswordPopout.qml @@ -183,14 +183,17 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter Layout.topMargin: Appearance.spacing.small - visible: connectButton.connecting + visible: connectButton.connecting || connectButton.hasError text: { + if (connectButton.hasError) { + return qsTr("Connection failed. Please check your password and try again."); + } if (connectButton.connecting) { return qsTr("Connecting..."); } return ""; } - color: Colours.palette.m3onSurfaceVariant + color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small font.weight: 400 wrapMode: Text.WordWrap @@ -215,6 +218,11 @@ ColumnLayout { forceActiveFocus(); } + // Clear error when user starts typing + if (connectButton.hasError && event.text && event.text.length > 0) { + connectButton.hasError = false; + } + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { if (connectButton.enabled) { connectButton.clicked(); @@ -241,6 +249,7 @@ ColumnLayout { passwordContainer.forceActiveFocus(); }, 50); passwordContainer.passwordBuffer = ""; + connectButton.hasError = false; } } } @@ -259,10 +268,16 @@ ColumnLayout { color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer - border.width: passwordContainer.activeFocus ? 4 : (root.shouldBeVisible ? 1 : 0) - border.color: passwordContainer.activeFocus - ? Colours.palette.m3primary - : (root.shouldBeVisible ? Colours.palette.m3outline : "transparent") + border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) + border.color: { + if (connectButton.hasError) { + return Colours.palette.m3error; + } + if (passwordContainer.activeFocus) { + return Colours.palette.m3primary; + } + return root.shouldBeVisible ? Colours.palette.m3outline : "transparent"; + } Behavior on border.color { CAnim {} @@ -409,6 +424,7 @@ ColumnLayout { enabled: passwordContainer.passwordBuffer.length > 0 && !connecting property bool connecting: false + property bool hasError: false onClicked: { if (!root.network || connecting) { @@ -420,6 +436,9 @@ ColumnLayout { return; } + // Clear any previous error + hasError = false; + // Set connecting state connecting = true; enabled = false; @@ -437,19 +456,26 @@ ColumnLayout { // Shouldn't happen since we provided password connectionMonitor.stop(); connecting = false; + hasError = true; enabled = true; text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid, () => {}); + } } else { - // Connection failed immediately - return to network popout + // Connection failed immediately - show error connectionMonitor.stop(); connecting = false; + hasError = true; enabled = true; text = qsTr("Connect"); - Qt.callLater(() => { - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; - } - }, 500); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid, () => {}); + } } } ); @@ -498,14 +524,14 @@ ColumnLayout { if (connectionMonitor.repeatCount > 10) { connectionMonitor.stop(); connectButton.connecting = false; + connectButton.hasError = true; connectButton.enabled = true; connectButton.text = qsTr("Connect"); - // Return to network popout on timeout failure - Qt.callLater(() => { - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; - } - }, 500); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid, () => {}); + } } } } @@ -540,14 +566,12 @@ ColumnLayout { if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { connectionMonitor.stop(); connectButton.connecting = false; + connectButton.hasError = true; connectButton.enabled = true; connectButton.text = qsTr("Connect"); - // Return to network popout on connection failure - Qt.callLater(() => { - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; - } - }, 500); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + Nmcli.forgetNetwork(ssid, () => {}); } } } @@ -560,6 +584,7 @@ ColumnLayout { isClosing = true; passwordContainer.passwordBuffer = ""; connectButton.connecting = false; + connectButton.hasError = false; connectButton.text = qsTr("Connect"); connectionMonitor.stop(); -- cgit v1.2.3-freya From a10c22480c9b70afbe96982404040249f3a22885 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 00:03:21 -0500 Subject: tray: wireless password popout, no more full-rounding buttons --- modules/bar/popouts/WirelessPasswordPopout.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bar/popouts/WirelessPasswordPopout.qml b/modules/bar/popouts/WirelessPasswordPopout.qml index 8bd256e..2fd063f 100644 --- a/modules/bar/popouts/WirelessPasswordPopout.qml +++ b/modules/bar/popouts/WirelessPasswordPopout.qml @@ -605,7 +605,7 @@ ColumnLayout { implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2 implicitHeight: rowLayout.implicitHeight + Appearance.padding.small - radius: Appearance.rounding.normal + radius: Appearance.rounding.small StateLayer { id: stateLayer -- cgit v1.2.3-freya From b1e5660c568a59af115acc2da1bca86c36a59efc Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 00:15:50 -0500 Subject: tray: open settings button matches other popouts --- modules/bar/popouts/Audio.qml | 2 +- modules/bar/popouts/Bluetooth.qml | 2 +- modules/controlcenter/network/SimpleButton.qml | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 20e98e5..40c09d3 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -110,7 +110,7 @@ Item { Layout.topMargin: Appearance.spacing.normal color: Colours.palette.m3primaryContainer onColor: Colours.palette.m3onPrimaryContainer - text: qsTr("Open Settings") + text: qsTr("Open settings") icon: "settings" onClicked: { diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 1415f89..63621c2 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -170,7 +170,7 @@ ColumnLayout { Layout.topMargin: Appearance.spacing.normal color: Colours.palette.m3primaryContainer onColor: Colours.palette.m3onPrimaryContainer - text: qsTr("Open Settings") + text: qsTr("Open settings") icon: "settings" onClicked: { diff --git a/modules/controlcenter/network/SimpleButton.qml b/modules/controlcenter/network/SimpleButton.qml index 7d85e4f..a73f751 100644 --- a/modules/controlcenter/network/SimpleButton.qml +++ b/modules/controlcenter/network/SimpleButton.qml @@ -15,7 +15,7 @@ StyledRect { property string icon: "" implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: rowLayout.implicitHeight + Appearance.padding.small + implicitHeight: rowLayout.implicitHeight + Appearance.padding.small * 2 radius: Appearance.rounding.normal StateLayer { @@ -38,12 +38,10 @@ StyledRect { visible: root.icon.length > 0 text: root.icon color: root.onColor - font.pointSize: Appearance.font.size.large } StyledText { id: label - Layout.leftMargin: root.icon.length > 0 ? Appearance.padding.smaller : 0 color: parent.parent.onColor } } -- cgit v1.2.3-freya From 7d1c89a7a85f45b229d341013e1001cce30b642e Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 00:18:20 -0500 Subject: controlcenter: undo accordion collapse on network and audio pane sections --- modules/controlcenter/audio/AudioPane.qml | 14 -------------- modules/controlcenter/network/NetworkingPane.qml | 14 -------------- 2 files changed, 28 deletions(-) diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 6f5a1f4..8c990a6 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -62,13 +62,6 @@ RowLayout { title: qsTr("Output devices") expanded: true - onToggleRequested: { - if (!expanded) { - // Opening output devices, close input devices - inputDevicesSection.expanded = false; - } - } - ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small @@ -149,13 +142,6 @@ RowLayout { title: qsTr("Input devices") expanded: true - onToggleRequested: { - if (!expanded) { - // Opening input devices, close output devices - outputDevicesSection.expanded = false; - } - } - ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index a44a54c..0560581 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -107,13 +107,6 @@ RowLayout { title: qsTr("Ethernet") expanded: true - onToggleRequested: { - if (!expanded) { - // Opening ethernet, close wireless - wirelessListSection.expanded = false; - } - } - ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small @@ -241,13 +234,6 @@ RowLayout { title: qsTr("Wireless") expanded: true - onToggleRequested: { - if (!expanded) { - // Opening wireless, close ethernet - ethernetListSection.expanded = false; - } - } - ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small -- cgit v1.2.3-freya From 90e920bf2e002b4e6e0aa1896687c4800d770605 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 00:25:31 -0500 Subject: controlcenter: fixed anchors vs parent logs --- components/controls/CollapsibleSection.qml | 2 +- modules/controlcenter/appearance/AppearancePane.qml | 12 ++++-------- modules/controlcenter/launcher/LauncherPane.qml | 3 +-- modules/controlcenter/network/NetworkingPane.qml | 3 ++- modules/controlcenter/taskbar/TaskbarPane.qml | 8 ++++---- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 5bec5f8..3fba1c3 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -49,7 +49,7 @@ ColumnLayout { font.pointSize: Appearance.font.size.normal Behavior on rotation { Anim { - duration: Appearance.anim.durations.short + duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.standard } } diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 719e9b3..5ddcb06 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -295,8 +295,7 @@ RowLayout { delegate: StyledRect { required property var modelData - anchors.left: parent.left - anchors.right: parent.right + width: parent ? parent.width : 0 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal @@ -386,8 +385,7 @@ RowLayout { delegate: StyledRect { required property var modelData - anchors.left: parent.left - anchors.right: parent.right + width: parent ? parent.width : 0 readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` readonly property bool isCurrent: schemeKey === Schemes.currentScheme @@ -427,9 +425,7 @@ RowLayout { RowLayout { id: schemeRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + anchors.fill: parent anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal @@ -437,7 +433,7 @@ RowLayout { StyledRect { id: preview - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter border.width: 1 border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index d0bd406..0645860 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -310,8 +310,7 @@ RowLayout { delegate: StyledRect { required property var modelData - anchors.left: parent.left - anchors.right: parent.right + width: parent ? parent.width : 0 readonly property bool isSelected: root.selectedApp === modelData diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 0560581..620e6f7 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -501,7 +501,8 @@ RowLayout { } WirelessPasswordDialog { - anchors.fill: parent + Layout.fillWidth: true + Layout.fillHeight: true session: root.session z: 1000 } diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 0dcc152..ee47683 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -250,10 +250,10 @@ RowLayout { RowLayout { id: clockRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + Layout.fillWidth: true + Layout.leftMargin: Appearance.padding.large + Layout.rightMargin: Appearance.padding.large + Layout.alignment: Qt.AlignVCenter spacing: Appearance.spacing.normal -- cgit v1.2.3-freya From 20e07b2a13036335c9890fe67708d2ab2292a4b4 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 00:29:42 -0500 Subject: controlcenter: clip to all styledflickable --- modules/controlcenter/Panes.qml | 2 ++ modules/controlcenter/appearance/AppearancePane.qml | 2 ++ modules/controlcenter/audio/AudioPane.qml | 1 + modules/controlcenter/bluetooth/BtPane.qml | 3 +++ modules/controlcenter/bluetooth/Details.qml | 1 + modules/controlcenter/ethernet/EthernetDetails.qml | 1 + modules/controlcenter/ethernet/EthernetPane.qml | 4 +++- modules/controlcenter/launcher/LauncherPane.qml | 1 + modules/controlcenter/network/NetworkingPane.qml | 5 ++++- modules/controlcenter/network/WirelessDetails.qml | 1 + modules/controlcenter/network/WirelessPane.qml | 1 + modules/controlcenter/taskbar/TaskbarPane.qml | 2 ++ 12 files changed, 22 insertions(+), 2 deletions(-) diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 8a46088..52ad7f2 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -19,12 +19,14 @@ ClippingRectangle { required property Session session color: "transparent" + clip: true ColumnLayout { id: layout spacing: 0 y: -root.session.activeIndex * root.height + clip: true Pane { index: 0 diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 5ddcb06..fd75ff4 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -225,6 +225,7 @@ RowLayout { anchors.fill: parent flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.implicitHeight + Appearance.padding.large * 2 + clip: true StyledScrollBar.vertical: StyledScrollBar { flickable: sidebarFlickable @@ -965,6 +966,7 @@ RowLayout { flickableDirection: Flickable.VerticalFlick contentHeight: contentLayout.implicitHeight + clip: true StyledScrollBar.vertical: StyledScrollBar { flickable: parent diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 8c990a6..0a7f602 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -31,6 +31,7 @@ RowLayout { anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 flickableDirection: Flickable.VerticalFlick contentHeight: leftContent.height + clip: true ColumnLayout { id: leftContent diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 96dc002..40cf356 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -50,6 +50,7 @@ RowLayout { radius: rightBorder.innerRadius color: "transparent" + clip: true Loader { id: loader @@ -59,6 +60,7 @@ RowLayout { anchors.fill: parent anchors.margins: Appearance.padding.large * 2 + clip: true asynchronous: true sourceComponent: pane ? details : settings @@ -106,6 +108,7 @@ RowLayout { StyledFlickable { flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height + clip: true Settings { id: settingsInner diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 104f673..cbccd9b 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -22,6 +22,7 @@ Item { anchors.fill: parent flickableDirection: Flickable.VerticalFlick + clip: true contentHeight: layout.height ColumnLayout { diff --git a/modules/controlcenter/ethernet/EthernetDetails.qml b/modules/controlcenter/ethernet/EthernetDetails.qml index 68510da..6a01a8b 100644 --- a/modules/controlcenter/ethernet/EthernetDetails.qml +++ b/modules/controlcenter/ethernet/EthernetDetails.qml @@ -34,6 +34,7 @@ Item { anchors.fill: parent flickableDirection: Flickable.VerticalFlick + clip: true contentHeight: layout.height ColumnLayout { diff --git a/modules/controlcenter/ethernet/EthernetPane.qml b/modules/controlcenter/ethernet/EthernetPane.qml index b3ff317..05d0b1b 100644 --- a/modules/controlcenter/ethernet/EthernetPane.qml +++ b/modules/controlcenter/ethernet/EthernetPane.qml @@ -50,6 +50,7 @@ RowLayout { radius: rightBorder.innerRadius color: "transparent" + clip: true Loader { id: loader @@ -64,7 +65,7 @@ RowLayout { scale: 1 transformOrigin: Item.Center - clip: false + clip: true asynchronous: true sourceComponent: pane ? details : settings @@ -120,6 +121,7 @@ RowLayout { StyledFlickable { flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height + clip: true EthernetSettings { id: settingsInner diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 0645860..01ed72e 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -421,6 +421,7 @@ RowLayout { anchors.fill: parent flickableDirection: Flickable.VerticalFlick contentHeight: debugLayout.implicitHeight + clip: true StyledScrollBar.vertical: StyledScrollBar { flickable: parent diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 620e6f7..2faa40a 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -36,6 +36,7 @@ RowLayout { anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 flickableDirection: Flickable.VerticalFlick contentHeight: leftContent.height + clip: true ColumnLayout { id: leftContent @@ -399,6 +400,7 @@ RowLayout { radius: rightBorder.innerRadius color: "transparent" + clip: true // Right pane - networking details/settings Loader { @@ -416,7 +418,7 @@ RowLayout { scale: 1 transformOrigin: Item.Center - clip: false + clip: true asynchronous: true sourceComponent: pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings @@ -472,6 +474,7 @@ RowLayout { StyledFlickable { flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height + clip: true NetworkSettings { id: settingsInner diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 334c4b3..f41451a 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -102,6 +102,7 @@ Item { anchors.fill: parent flickableDirection: Flickable.VerticalFlick + clip: true contentHeight: layout.height ColumnLayout { diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index 9d48729..67a00f7 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -120,6 +120,7 @@ RowLayout { StyledFlickable { flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height + clip: true WirelessSettings { id: settingsInner diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index ee47683..72e9a80 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -209,6 +209,7 @@ RowLayout { anchors.fill: parent flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.implicitHeight + Appearance.padding.large * 2 + clip: true StyledScrollBar.vertical: StyledScrollBar { flickable: sidebarFlickable @@ -608,6 +609,7 @@ RowLayout { flickableDirection: Flickable.VerticalFlick contentHeight: contentLayout.implicitHeight + clip: true StyledScrollBar.vertical: StyledScrollBar { flickable: parent -- cgit v1.2.3-freya From e339439ee885345b44bff6e2f9db9dbffacfcfbd Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 00:31:19 -0500 Subject: controlcenter: network pane added scrollbar --- modules/controlcenter/ethernet/EthernetDetails.qml | 6 ++++++ modules/controlcenter/network/NetworkingPane.qml | 12 ++++++++++++ modules/controlcenter/network/WirelessDetails.qml | 6 ++++++ 3 files changed, 24 insertions(+) diff --git a/modules/controlcenter/ethernet/EthernetDetails.qml b/modules/controlcenter/ethernet/EthernetDetails.qml index 6a01a8b..f715970 100644 --- a/modules/controlcenter/ethernet/EthernetDetails.qml +++ b/modules/controlcenter/ethernet/EthernetDetails.qml @@ -31,12 +31,18 @@ Item { } StyledFlickable { + id: flickable + anchors.fill: parent flickableDirection: Flickable.VerticalFlick clip: true contentHeight: layout.height + StyledScrollBar.vertical: StyledScrollBar { + flickable: flickable + } + ColumnLayout { id: layout diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 2faa40a..5d93ff9 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -30,6 +30,8 @@ RowLayout { // Left pane - networking list with collapsible sections StyledFlickable { + id: leftFlickable + anchors.fill: parent anchors.margins: Appearance.padding.large + Appearance.padding.normal anchors.leftMargin: Appearance.padding.large @@ -38,6 +40,10 @@ RowLayout { contentHeight: leftContent.height clip: true + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } + ColumnLayout { id: leftContent @@ -472,10 +478,16 @@ RowLayout { id: settings StyledFlickable { + id: settingsFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height clip: true + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } + NetworkSettings { id: settingsInner diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index f41451a..5b4f541 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -99,12 +99,18 @@ Item { } StyledFlickable { + id: flickable + anchors.fill: parent flickableDirection: Flickable.VerticalFlick clip: true contentHeight: layout.height + StyledScrollBar.vertical: StyledScrollBar { + flickable: flickable + } + ColumnLayout { id: layout -- cgit v1.2.3-freya From 23c1c02a3309e8b33da2aa720f6c4b07924d6648 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 00:33:38 -0500 Subject: controlpanel: maintain proper size --- modules/controlcenter/audio/AudioPane.qml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 0a7f602..d6989cd 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -228,11 +228,28 @@ RowLayout { Layout.fillWidth: true Layout.fillHeight: true - ColumnLayout { + StyledFlickable { + id: rightFlickable + anchors.fill: parent anchors.margins: Appearance.padding.large * 2 - spacing: Appearance.spacing.normal + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.implicitHeight + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: rightFlickable + } + + ColumnLayout { + id: contentLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + + spacing: Appearance.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter @@ -370,6 +387,7 @@ RowLayout { onMoved: Audio.setSourceVolume(value) } } + } InnerBorder { leftThickness: Appearance.padding.normal / 2 -- cgit v1.2.3-freya From a40624839bba5cb429c34b9461b817a4a3434d02 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 00:36:26 -0500 Subject: controlcenter: selectables now match bluetooth pane --- modules/controlcenter/audio/AudioPane.qml | 4 ---- modules/controlcenter/launcher/LauncherPane.qml | 2 -- modules/controlcenter/network/NetworkingPane.qml | 4 ---- 3 files changed, 10 deletions(-) diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index d6989cd..ca54f77 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -95,8 +95,6 @@ RowLayout { color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Audio.sink?.id === modelData.id ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal - border.width: Audio.sink?.id === modelData.id ? 1 : 0 - border.color: Colours.palette.m3primary StateLayer { function onClicked(): void { @@ -175,8 +173,6 @@ RowLayout { color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Audio.source?.id === modelData.id ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal - border.width: Audio.source?.id === modelData.id ? 1 : 0 - border.color: Colours.palette.m3primary StateLayer { function onClicked(): void { diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 01ed72e..92e17ee 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -316,8 +316,6 @@ RowLayout { color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isSelected ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal - border.width: isSelected ? 1 : 0 - border.color: Colours.palette.m3primary StateLayer { function onClicked(): void { diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 5d93ff9..c605a36 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -148,8 +148,6 @@ RowLayout { color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal - border.width: root.session.ethernet.active === modelData ? 1 : 0 - border.color: Colours.palette.m3primary StateLayer { function onClicked(): void { @@ -290,8 +288,6 @@ RowLayout { color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.network.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal - border.width: root.session.network.active === modelData ? 1 : 0 - border.color: Colours.palette.m3primary StateLayer { function onClicked(): void { -- cgit v1.2.3-freya From f204e860ed582b05965936bf470cdfc9649d2b3a Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 00:38:04 -0500 Subject: controlcenter: launcher pane only shows hide when app selected --- modules/controlcenter/launcher/LauncherPane.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 92e17ee..0a1175f 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -434,6 +434,7 @@ RowLayout { SwitchRow { Layout.topMargin: Appearance.spacing.normal + visible: root.selectedApp !== null label: qsTr("Hide from launcher") checked: root.hideFromLauncherChecked enabled: root.selectedApp !== null && configFile.loaded -- cgit v1.2.3-freya From 6cc779d6f3e7067ed38b8c7b1001827f26314cf1 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 00:50:17 -0500 Subject: controlcenter: wireless networks display icons of signal strength --- modules/controlcenter/network/NetworkingPane.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index c605a36..c834aaa 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -9,6 +9,7 @@ import qs.components.effects import qs.components.containers import qs.services import qs.config +import qs.utils import Quickshell import Quickshell.Widgets import QtQuick @@ -321,7 +322,7 @@ RowLayout { id: wirelessIcon anchors.centerIn: parent - text: modelData.isSecure ? "lock" : "wifi" + text: Icons.getNetworkIcon(modelData.strength) font.pointSize: Appearance.font.size.large fill: modelData.active ? 1 : 0 color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface -- cgit v1.2.3-freya From a86fde606e8542b40856c79fa0307fca99f1f7d2 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 00:53:10 -0500 Subject: controlcenter: audio pane refresh --- modules/controlcenter/audio/AudioPane.qml | 219 ++++++++++++++++-------------- 1 file changed, 114 insertions(+), 105 deletions(-) diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index ca54f77..32d6680 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -247,142 +247,151 @@ RowLayout { spacing: Appearance.spacing.normal - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "volume_up" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } + ConnectionHeader { + icon: "volume_up" + title: qsTr("Audio Settings") + } - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Audio Settings") - font.pointSize: Appearance.font.size.large - font.bold: true - } + SectionHeader { + title: qsTr("Output volume") + description: qsTr("Control the volume of your output device") + } - StyledText { - Layout.topMargin: Appearance.spacing.large - Layout.alignment: Qt.AlignHCenter - text: qsTr("Output volume") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + SectionContainer { + contentSpacing: Appearance.spacing.normal - RowLayout { - spacing: Appearance.spacing.smaller + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - StyledText { - text: qsTr("Volume") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - Item { - Layout.fillWidth: true - } + StyledText { + text: qsTr("Volume") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: Audio.muted ? qsTr("Muted") : qsTr("%1%").arg(Math.round(Audio.volume * 100)) + color: Audio.muted ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } - 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 { + 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) + } } } - } - StyledText { - text: Audio.muted ? qsTr("Muted") : qsTr("%1%").arg(Math.round(Audio.volume * 100)) - color: Audio.muted ? Colours.palette.m3primary : Colours.palette.m3outline - } + SectionHeader { + title: qsTr("Input volume") + description: qsTr("Control the volume of your input device") + } - StyledSlider { - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + SectionContainer { + contentSpacing: Appearance.spacing.normal - value: Audio.volume - enabled: !Audio.muted - opacity: enabled ? 1 : 0.5 - onMoved: Audio.setVolume(value) - } + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - StyledText { - Layout.topMargin: Appearance.spacing.large - Layout.alignment: Qt.AlignHCenter - text: qsTr("Input volume") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - RowLayout { - spacing: Appearance.spacing.smaller + StyledText { + text: qsTr("Volume") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } - StyledText { - text: qsTr("Volume") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } + Item { + Layout.fillWidth: true + } - Item { - Layout.fillWidth: true - } + StyledText { + text: Audio.sourceMuted ? qsTr("Muted") : qsTr("%1%").arg(Math.round(Audio.sourceVolume * 100)) + color: Audio.sourceMuted ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - StyledRect { - implicitWidth: implicitHeight - implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2 + StateLayer { + function onClicked(): void { + if (Audio.source?.audio) { + Audio.source.audio.muted = !Audio.source.audio.muted; + } + } + } - radius: Appearance.rounding.normal - color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + MaterialIcon { + id: muteInputIcon - StateLayer { - function onClicked(): void { - if (Audio.source?.audio) { - Audio.source.audio.muted = !Audio.source.audio.muted; + anchors.centerIn: parent + text: Audio.sourceMuted ? "mic_off" : "mic" + color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + } } } - } - MaterialIcon { - id: muteInputIcon + StyledSlider { + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 - anchors.centerIn: parent - text: Audio.sourceMuted ? "mic_off" : "mic" - color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + value: Audio.sourceVolume + enabled: !Audio.sourceMuted + opacity: enabled ? 1 : 0.5 + onMoved: Audio.setSourceVolume(value) + } } } } - - StyledText { - text: Audio.sourceMuted ? qsTr("Muted") : qsTr("%1%").arg(Math.round(Audio.sourceVolume * 100)) - color: Audio.sourceMuted ? Colours.palette.m3primary : Colours.palette.m3outline - } - - StyledSlider { - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - value: Audio.sourceVolume - enabled: !Audio.sourceMuted - opacity: enabled ? 1 : 0.5 - onMoved: Audio.setSourceVolume(value) - } - } } InnerBorder { -- cgit v1.2.3-freya From 19fb58d249a0036bf454a07beb7793798ac50926 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 00:59:19 -0500 Subject: controlcenter: removed signal strength percentage from network list --- modules/controlcenter/network/NetworkingPane.qml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index c834aaa..56ab7f1 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -344,12 +344,6 @@ RowLayout { font.weight: modelData.active ? 500 : 400 } - StyledText { - text: qsTr("%1%").arg(modelData.strength) - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - StyledRect { implicitWidth: implicitHeight implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2 -- cgit v1.2.3-freya From 6a92f985d8739ef2c397714f79e18b74f48fb705 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:15:02 +1100 Subject: internal: use existing button controls Instead of SimpleButton --- components/controls/IconTextButton.qml | 9 +- modules/bar/popouts/Audio.qml | 11 +- modules/bar/popouts/Bluetooth.qml | 11 +- modules/bar/popouts/Network.qml | 15 +- modules/bar/popouts/WirelessPasswordPopout.qml | 165 +++++++-------------- modules/controlcenter/network/SimpleButton.qml | 50 ------- modules/controlcenter/network/WirelessDetails.qml | 44 +++--- .../network/WirelessPasswordDialog.qml | 54 +++---- utils/Icons.qml | 1 + 9 files changed, 120 insertions(+), 240 deletions(-) delete mode 100644 modules/controlcenter/network/SimpleButton.qml diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml index 78e7c5b..0badd7a 100644 --- a/components/controls/IconTextButton.qml +++ b/components/controls/IconTextButton.qml @@ -2,6 +2,7 @@ import ".." import qs.services import qs.config import QtQuick +import QtQuick.Layouts StyledRect { id: root @@ -53,7 +54,7 @@ StyledRect { } } - Row { + RowLayout { id: row anchors.centerIn: parent @@ -62,7 +63,8 @@ StyledRect { MaterialIcon { id: iconLabel - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter + Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575) color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour fill: root.internalChecked ? 1 : 0 @@ -74,7 +76,8 @@ StyledRect { StyledText { id: label - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter + Layout.topMargin: -Math.round(iconLabel.fontInfo.pointSize * 0.0575) color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour } } diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 40c09d3..58b29ba 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -105,17 +105,16 @@ Item { } } - SimpleButton { + IconTextButton { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal - color: Colours.palette.m3primaryContainer - onColor: Colours.palette.m3onPrimaryContainer + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer + verticalPadding: Appearance.padding.small text: qsTr("Open settings") icon: "settings" - onClicked: { - root.wrapper.detach("audio"); - } + onClicked: root.wrapper.detach("audio") } } } diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 63621c2..4674905 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -165,17 +165,16 @@ ColumnLayout { } } - SimpleButton { + IconTextButton { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal - color: Colours.palette.m3primaryContainer - onColor: Colours.palette.m3onPrimaryContainer + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer + verticalPadding: Appearance.padding.small text: qsTr("Open settings") icon: "settings" - onClicked: { - root.wrapper.detach("bluetooth"); - } + onClicked: root.wrapper.detach("bluetooth") } component Toggle: RowLayout { diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 93ff867..b9f66c4 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -111,10 +111,8 @@ ColumnLayout { } StyledRect { - id: connectBtn - implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.small + implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.small radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0) @@ -136,7 +134,7 @@ ColumnLayout { // Check if network is secure if (networkItem.modelData.isSecure) { // Try to connect first - will show password dialog if password is needed - Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, (result) => { + Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, result => { if (result && result.needsPassword) { // Password is required - show password dialog root.passwordNetwork = networkItem.modelData; @@ -159,7 +157,7 @@ ColumnLayout { } MaterialIcon { - id: connectIcon + id: wirelessConnectIcon anchors.centerIn: parent animate: true @@ -205,12 +203,14 @@ ColumnLayout { MaterialIcon { id: scanIcon + Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575) animate: true text: "wifi_find" color: Colours.palette.m3onPrimaryContainer } StyledText { + Layout.topMargin: -Math.round(scanIcon.fontInfo.pointSize * 0.0575) text: qsTr("Rescan networks") color: Colours.palette.m3onPrimaryContainer } @@ -303,8 +303,6 @@ ColumnLayout { } StyledRect { - id: connectBtn - implicitWidth: implicitHeight implicitHeight: connectIcon.implicitHeight + Appearance.padding.small @@ -354,8 +352,7 @@ ColumnLayout { if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { root.connectingToSsid = ""; // Close password dialog if we successfully connected - if (root.showPasswordDialog && root.passwordNetwork && - Nmcli.active.ssid === root.passwordNetwork.ssid) { + if (root.showPasswordDialog && root.passwordNetwork && Nmcli.active.ssid === root.passwordNetwork.ssid) { root.showPasswordDialog = false; root.passwordNetwork = null; if (root.wrapper.currentName === "wirelesspassword") { diff --git a/modules/bar/popouts/WirelessPasswordPopout.qml b/modules/bar/popouts/WirelessPasswordPopout.qml index 2fd063f..aa7f40f 100644 --- a/modules/bar/popouts/WirelessPasswordPopout.qml +++ b/modules/bar/popouts/WirelessPasswordPopout.qml @@ -41,14 +41,14 @@ ColumnLayout { } spacing: Appearance.spacing.normal - + implicitWidth: 400 implicitHeight: content.implicitHeight + Appearance.padding.large * 2 visible: shouldBeVisible || isClosing enabled: shouldBeVisible && !isClosing focus: enabled - + Component.onCompleted: { if (shouldBeVisible) { Qt.callLater(() => { @@ -57,7 +57,7 @@ ColumnLayout { }, 150); } } - + onShouldBeVisibleChanged: { if (shouldBeVisible) { Qt.callLater(() => { @@ -67,9 +67,7 @@ ColumnLayout { } } - Keys.onEscapePressed: { - closeDialog(); - } + Keys.onEscapePressed: closeDialog() StyledRect { Layout.fillWidth: true @@ -109,7 +107,7 @@ ColumnLayout { } } - Keys.onEscapePressed: closeDialog(); + Keys.onEscapePressed: root.closeDialog() ColumnLayout { id: content @@ -149,7 +147,7 @@ ColumnLayout { color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } - + Timer { interval: 50 running: root.shouldBeVisible && (!root.network || !root.network.ssid) @@ -217,12 +215,12 @@ ColumnLayout { if (!activeFocus) { forceActiveFocus(); } - + // Clear error when user starts typing if (connectButton.hasError && event.text && event.text.length > 0) { connectButton.hasError = false; } - + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { if (connectButton.enabled) { connectButton.clicked(); @@ -253,7 +251,7 @@ ColumnLayout { } } } - + Component.onCompleted: { if (root.shouldBeVisible) { Qt.callLater(() => { @@ -265,9 +263,7 @@ ColumnLayout { StyledRect { anchors.fill: parent radius: Appearance.rounding.normal - color: passwordContainer.activeFocus - ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) - : Colours.tPalette.m3surfaceContainer + color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) border.color: { if (connectButton.hasError) { @@ -401,31 +397,31 @@ ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal - SimpleButton { + TextButton { id: cancelButton Layout.fillWidth: true Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 - color: Colours.palette.m3secondaryContainer - onColor: Colours.palette.m3onSecondaryContainer + inactiveColour: Colours.palette.m3secondaryContainer + inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Cancel") - onClicked: closeDialog(); + onClicked: root.closeDialog() } - SimpleButton { + TextButton { id: connectButton + property bool connecting: false + property bool hasError: false + Layout.fillWidth: true Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 - color: Colours.palette.m3primary - onColor: Colours.palette.m3onPrimary + inactiveColour: Colours.palette.m3primary + inactiveOnColour: Colours.palette.m3onPrimary text: qsTr("Connect") enabled: passwordContainer.passwordBuffer.length > 0 && !connecting - property bool connecting: false - property bool hasError: false - onClicked: { if (!root.network || connecting) { return; @@ -445,40 +441,35 @@ ColumnLayout { text = qsTr("Connecting..."); // Connect to network - Nmcli.connectToNetwork( - root.network.ssid, - password, - root.network.bssid || "", - (result) => { - if (result && result.success) { - // Connection successful, monitor will handle the rest - } else if (result && result.needsPassword) { - // Shouldn't happen since we provided password - connectionMonitor.stop(); - connecting = false; - hasError = true; - enabled = true; - text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid, () => {}); - } - } else { - // Connection failed immediately - show error - connectionMonitor.stop(); - connecting = false; - hasError = true; - enabled = true; - text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid, () => {}); - } + Nmcli.connectToNetwork(root.network.ssid, password, root.network.bssid || "", result => { + if (result && result.success) + // Connection successful, monitor will handle the rest + {} else if (result && result.needsPassword) { + // Shouldn't happen since we provided password + connectionMonitor.stop(); + connecting = false; + hasError = true; + enabled = true; + text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } else { + // Connection failed immediately - show error + connectionMonitor.stop(); + connecting = false; + hasError = true; + enabled = true; + text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); } } - ); + }); // Start monitoring connection connectionMonitor.start(); @@ -494,8 +485,7 @@ ColumnLayout { } // Check if we're connected to the target network (case-insensitive SSID comparison) - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && - Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); if (isConnected) { // Successfully connected - give it a moment for network list to update @@ -530,7 +520,7 @@ ColumnLayout { passwordContainer.passwordBuffer = ""; // Delete the failed connection if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid, () => {}); + Nmcli.forgetNetwork(root.network.ssid); } } } @@ -545,7 +535,7 @@ ColumnLayout { onTriggered: { repeatCount++; - checkConnectionStatus(); + root.checkConnectionStatus(); } onRunningChanged: { @@ -559,7 +549,7 @@ ColumnLayout { target: Nmcli function onActiveChanged() { if (root.shouldBeVisible) { - checkConnectionStatus(); + root.checkConnectionStatus(); } } function onConnectionFailed(ssid: string) { @@ -571,7 +561,7 @@ ColumnLayout { connectButton.text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; // Delete the failed connection - Nmcli.forgetNetwork(ssid, () => {}); + Nmcli.forgetNetwork(ssid); } } } @@ -580,64 +570,17 @@ ColumnLayout { if (isClosing) { return; } - + isClosing = true; passwordContainer.passwordBuffer = ""; connectButton.connecting = false; connectButton.hasError = false; connectButton.text = qsTr("Connect"); connectionMonitor.stop(); - + // Return to network popout if (root.wrapper.currentName === "wirelesspassword") { root.wrapper.currentName = "network"; } } - - component SimpleButton: StyledRect { - id: button - - property color onColor: Colours.palette.m3onSurface - property alias disabled: stateLayer.disabled - property alias text: label.text - property alias enabled: stateLayer.enabled - property string icon: "" - - implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: rowLayout.implicitHeight + Appearance.padding.small - radius: Appearance.rounding.small - - StateLayer { - id: stateLayer - color: parent.onColor - function onClicked(): void { - if (parent.enabled !== false) { - parent.clicked(); - } - } - } - - RowLayout { - id: rowLayout - anchors.centerIn: parent - spacing: Appearance.spacing.small - - MaterialIcon { - id: iconItem - visible: button.icon.length > 0 - text: button.icon - color: button.onColor - font.pointSize: Appearance.font.size.large - } - - StyledText { - id: label - Layout.leftMargin: button.icon.length > 0 ? Appearance.padding.smaller : 0 - color: parent.parent.onColor - } - } - - signal clicked - } } - diff --git a/modules/controlcenter/network/SimpleButton.qml b/modules/controlcenter/network/SimpleButton.qml deleted file mode 100644 index a73f751..0000000 --- a/modules/controlcenter/network/SimpleButton.qml +++ /dev/null @@ -1,50 +0,0 @@ -import qs.components -import qs.components.controls -import qs.components.effects -import qs.config -import QtQuick -import QtQuick.Layouts - -StyledRect { - id: root - - property color onColor: Colours.palette.m3onSurface - property alias disabled: stateLayer.disabled - property alias text: label.text - property alias enabled: stateLayer.enabled - property string icon: "" - - implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: rowLayout.implicitHeight + Appearance.padding.small * 2 - radius: Appearance.rounding.normal - - StateLayer { - id: stateLayer - color: parent.onColor - function onClicked(): void { - if (parent.enabled !== false) { - parent.clicked(); - } - } - } - - RowLayout { - id: rowLayout - anchors.centerIn: parent - spacing: Appearance.spacing.small - - MaterialIcon { - id: iconItem - visible: root.icon.length > 0 - text: root.icon - color: root.onColor - } - - StyledText { - id: label - color: parent.parent.onColor - } - } - - signal clicked -} \ No newline at end of file diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 5b4f541..651f1fb 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -69,7 +69,7 @@ Item { if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { // Network is active but we don't have details yet, fetch them Nmcli.getWirelessDeviceDetails("", () => { - // After fetching, check if we got details - if not, timer will try again + // After fetching, check if we got details - if not, timer will try again }); } else { // We have details, can stop the timer @@ -89,7 +89,7 @@ Item { if (network && network.ssid) { const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); if (isActive) { - Nmcli.getWirelessDeviceDetails("", () => {}); + Nmcli.getWirelessDeviceDetails(""); } else { Nmcli.wirelessDeviceDetails = null; } @@ -134,14 +134,14 @@ Item { checked: root.network?.active ?? false toggle.onToggled: { if (checked) { - handleConnect(); + root.handleConnect(); } else { Nmcli.disconnectFromNetwork(); } } } - SimpleButton { + TextButton { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 @@ -151,8 +151,8 @@ Item { } return Nmcli.hasSavedProfile(root.network.ssid); } - color: Colours.palette.m3errorContainer - onColor: Colours.palette.m3onErrorContainer + inactiveColour: Colours.palette.m3errorContainer + inactiveOnColour: Colours.palette.m3onErrorContainer text: qsTr("Forget Network") onClicked: { @@ -160,7 +160,7 @@ Item { if (root.network.active) { Nmcli.disconnectFromNetwork(); } - Nmcli.forgetNetwork(root.network.ssid, () => {}); + Nmcli.forgetNetwork(root.network.ssid); } } } @@ -214,7 +214,6 @@ Item { deviceDetails: Nmcli.wirelessDeviceDetails } } - } } @@ -236,26 +235,21 @@ Item { if (hasSavedProfile) { Nmcli.connectToNetwork(root.network.ssid, "", root.network.bssid, null); } else { - Nmcli.connectToNetworkWithPasswordCheck( - root.network.ssid, - root.network.isSecure, - (result) => { - if (result.needsPassword) { - if (Nmcli.pendingConnection) { - Nmcli.connectionCheckTimer.stop(); - Nmcli.immediateCheckTimer.stop(); - Nmcli.immediateCheckTimer.checkCount = 0; - Nmcli.pendingConnection = null; - } - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = root.network; + Nmcli.connectToNetworkWithPasswordCheck(root.network.ssid, root.network.isSecure, result => { + if (result.needsPassword) { + if (Nmcli.pendingConnection) { + Nmcli.connectionCheckTimer.stop(); + Nmcli.immediateCheckTimer.stop(); + Nmcli.immediateCheckTimer.checkCount = 0; + Nmcli.pendingConnection = null; } - }, - root.network.bssid - ); + root.session.network.showPasswordDialog = true; + root.session.network.pendingNetwork = root.network; + } + }, root.network.bssid); } } else { Nmcli.connectToNetwork(root.network.ssid, "", root.network.bssid, null); } } -} \ No newline at end of file +} diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index d0c023f..f3381b7 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -48,7 +48,7 @@ Item { MouseArea { anchors.fill: parent - onClicked: closeDialog(); + onClicked: closeDialog() } } @@ -94,7 +94,7 @@ Item { } } - Keys.onEscapePressed: closeDialog(); + Keys.onEscapePressed: closeDialog() ColumnLayout { id: content @@ -314,25 +314,25 @@ Item { Layout.fillWidth: true spacing: Appearance.spacing.normal - SimpleButton { + TextButton { id: cancelButton Layout.fillWidth: true Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 - color: Colours.palette.m3secondaryContainer - onColor: Colours.palette.m3onSecondaryContainer + inactiveColour: Colours.palette.m3secondaryContainer + inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Cancel") - onClicked: closeDialog(); + onClicked: root.closeDialog() } - SimpleButton { + TextButton { id: connectButton Layout.fillWidth: true Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 - color: Colours.palette.m3primary - onColor: Colours.palette.m3onPrimary + inactiveColour: Colours.palette.m3primary + inactiveOnColour: Colours.palette.m3onPrimary text: qsTr("Connect") enabled: passwordContainer.passwordBuffer.length > 0 && !connecting @@ -354,24 +354,19 @@ Item { text = qsTr("Connecting..."); // Connect to network - Nmcli.connectToNetwork( - root.network.ssid, - password, - root.network.bssid || "", - (result) => { - if (result && result.success) { - // Connection successful, monitor will handle the rest - } else if (result && result.needsPassword) { - // Shouldn't happen since we provided password - connectionMonitor.stop(); - connecting = false; - enabled = true; - text = qsTr("Connect"); - } else { - // Connection failed, monitor will handle timeout - } - } - ); + Nmcli.connectToNetwork(root.network.ssid, password, root.network.bssid || "", result => { + if (result && result.success) + // Connection successful, monitor will handle the rest + {} else if (result && result.needsPassword) { + // Shouldn't happen since we provided password + connectionMonitor.stop(); + connecting = false; + enabled = true; + text = qsTr("Connect"); + } else + // Connection failed, monitor will handle timeout + {} + }); // Start monitoring connection connectionMonitor.start(); @@ -387,8 +382,7 @@ Item { } // Check if we're connected to the target network (case-insensitive SSID comparison) - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && - Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); if (isConnected) { // Successfully connected - give it a moment for network list to update @@ -459,7 +453,7 @@ Item { if (isClosing) { return; } - + isClosing = true; passwordContainer.passwordBuffer = ""; connectButton.connecting = false; diff --git a/utils/Icons.qml b/utils/Icons.qml index 389eca3..b3fe8ab 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -3,6 +3,7 @@ pragma Singleton import qs.config import Quickshell import Quickshell.Services.Notifications +import QtQuick Singleton { id: root -- cgit v1.2.3-freya From 05b0660627586dc7624380e82b818b53004771f5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 01:31:53 -0500 Subject: controlcenter: password input errors/wrong pass --- modules/bar/popouts/WirelessPasswordPopout.qml | 85 ++- modules/controlcenter/network/NetworkingPane.qml | 691 +++++++++++---------- .../network/WirelessPasswordDialog.qml | 115 +++- services/Nmcli.qml | 10 +- 4 files changed, 502 insertions(+), 399 deletions(-) diff --git a/modules/bar/popouts/WirelessPasswordPopout.qml b/modules/bar/popouts/WirelessPasswordPopout.qml index aa7f40f..59a15b9 100644 --- a/modules/bar/popouts/WirelessPasswordPopout.qml +++ b/modules/bar/popouts/WirelessPasswordPopout.qml @@ -32,14 +32,22 @@ ColumnLayout { } } // Force focus to password container when popout becomes active - Qt.callLater(() => { - passwordContainer.forceActiveFocus(); - }, 100); - }, 100); + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + }); } } } + Timer { + id: focusTimer + interval: 150 + onTriggered: { + root.forceActiveFocus(); + passwordContainer.forceActiveFocus(); + } + } + spacing: Appearance.spacing.normal implicitWidth: 400 @@ -51,19 +59,15 @@ ColumnLayout { Component.onCompleted: { if (shouldBeVisible) { - Qt.callLater(() => { - root.forceActiveFocus(); - passwordContainer.forceActiveFocus(); - }, 150); + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); } } onShouldBeVisibleChanged: { if (shouldBeVisible) { - Qt.callLater(() => { - root.forceActiveFocus(); - passwordContainer.forceActiveFocus(); - }, 150); + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); } } @@ -243,20 +247,26 @@ ColumnLayout { target: root function onShouldBeVisibleChanged(): void { if (root.shouldBeVisible) { - Qt.callLater(() => { - passwordContainer.forceActiveFocus(); - }, 50); + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); passwordContainer.passwordBuffer = ""; connectButton.hasError = false; } } } + Timer { + id: passwordFocusTimer + interval: 50 + onTriggered: { + passwordContainer.forceActiveFocus(); + } + } + Component.onCompleted: { if (root.shouldBeVisible) { - Qt.callLater(() => { - passwordContainer.forceActiveFocus(); - }, 100); + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); } } @@ -489,22 +499,8 @@ ColumnLayout { if (isConnected) { // Successfully connected - give it a moment for network list to update - Qt.callLater(() => { - // Double-check connection is still active - if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { - const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - if (stillConnected) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - // Return to network popout on successful connection - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; - } - closeDialog(); - } - } - }, 500); + // Use Timer for actual delay + connectionSuccessTimer.start(); return; } @@ -545,6 +541,27 @@ ColumnLayout { } } + Timer { + id: connectionSuccessTimer + interval: 500 + onTriggered: { + // Double-check connection is still active + if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { + const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + // Return to network popout on successful connection + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + closeDialog(); + } + } + } + } + Connections { target: Nmcli function onActiveChanged() { diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 56ab7f1..74e0034 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -15,500 +15,507 @@ import Quickshell.Widgets import QtQuick import QtQuick.Layouts -RowLayout { +Item { id: root required property Session session anchors.fill: parent - spacing: 0 + RowLayout { + id: contentLayout - Item { - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true + anchors.fill: parent + spacing: 0 - // Left pane - networking list with collapsible sections - StyledFlickable { - id: leftFlickable + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true - 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 - flickableDirection: Flickable.VerticalFlick - contentHeight: leftContent.height - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: leftFlickable - } + // Left pane - networking list with collapsible sections + StyledFlickable { + id: leftFlickable - ColumnLayout { - id: leftContent + 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 + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContent.height + clip: true - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.normal + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } - // Settings header above the collapsible sections - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.smaller + ColumnLayout { + id: leftContent - StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal - Item { + // Settings header above the collapsible sections + RowLayout { Layout.fillWidth: true - } + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } - ToggleButton { - toggled: Nmcli.wifiEnabled - icon: "wifi" - accent: "Tertiary" + Item { + Layout.fillWidth: true + } - onClicked: { - Nmcli.toggleWifi(null); + ToggleButton { + toggled: Nmcli.wifiEnabled + icon: "wifi" + accent: "Tertiary" + + onClicked: { + Nmcli.toggleWifi(null); + } } - } - ToggleButton { - toggled: Nmcli.scanning - icon: "wifi_find" - accent: "Secondary" + ToggleButton { + toggled: Nmcli.scanning + icon: "wifi_find" + accent: "Secondary" - onClicked: { - Nmcli.rescanWifi(); + onClicked: { + Nmcli.rescanWifi(); + } } - } - ToggleButton { - toggled: !root.session.ethernet.active && !root.session.network.active - icon: "settings" - accent: "Primary" - - onClicked: { - if (root.session.ethernet.active || root.session.network.active) { - root.session.ethernet.active = null; - root.session.network.active = null; - } else { - // Toggle to show settings - prefer ethernet if available, otherwise wireless - if (Nmcli.ethernetDevices.length > 0) { - root.session.ethernet.active = Nmcli.ethernetDevices[0]; - } else if (Nmcli.networks.length > 0) { - root.session.network.active = Nmcli.networks[0]; + ToggleButton { + toggled: !root.session.ethernet.active && !root.session.network.active + icon: "settings" + accent: "Primary" + + onClicked: { + if (root.session.ethernet.active || root.session.network.active) { + root.session.ethernet.active = null; + root.session.network.active = null; + } else { + // Toggle to show settings - prefer ethernet if available, otherwise wireless + if (Nmcli.ethernetDevices.length > 0) { + root.session.ethernet.active = Nmcli.ethernetDevices[0]; + } else if (Nmcli.networks.length > 0) { + root.session.network.active = Nmcli.networks[0]; + } } } } } - } - - CollapsibleSection { - id: ethernetListSection - Layout.fillWidth: true - title: qsTr("Ethernet") - expanded: true + CollapsibleSection { + id: ethernetListSection - ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + title: qsTr("Ethernet") + expanded: true - RowLayout { + ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small - StyledText { - text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) - font.pointSize: Appearance.font.size.normal - font.weight: 500 + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } } - } - StyledText { - Layout.fillWidth: true - text: qsTr("All available ethernet devices") - color: Colours.palette.m3outline - } + StyledText { + Layout.fillWidth: true + text: qsTr("All available ethernet devices") + color: Colours.palette.m3outline + } - Repeater { - id: ethernetRepeater + Repeater { + id: ethernetRepeater - Layout.fillWidth: true - model: Nmcli.ethernetDevices + Layout.fillWidth: true + model: Nmcli.ethernetDevices - delegate: StyledRect { - required property var modelData + delegate: StyledRect { + required property var modelData - Layout.fillWidth: true + Layout.fillWidth: true - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal - StateLayer { - function onClicked(): void { - root.session.network.active = null; - root.session.ethernet.active = modelData; + StateLayer { + function onClicked(): void { + root.session.network.active = null; + root.session.ethernet.active = modelData; + } } - } - RowLayout { - id: rowLayout + RowLayout { + id: rowLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledRect { - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.normal - color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.normal + color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - MaterialIcon { - id: icon + MaterialIcon { + id: icon - anchors.centerIn: parent - text: "cable" - font.pointSize: Appearance.font.size.large - fill: modelData.connected ? 1 : 0 - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + anchors.centerIn: parent + text: "cable" + font.pointSize: Appearance.font.size.large + fill: modelData.connected ? 1 : 0 + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } } - } - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 - text: modelData.interface || qsTr("Unknown") - } + text: modelData.interface || qsTr("Unknown") + } - StyledText { - text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") - color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: modelData.connected ? 500 : 400 - } + StyledText { + text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.connected ? 500 : 400 + } - StyledRect { - implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) - StateLayer { - function onClicked(): void { - if (modelData.connected && modelData.connection) { - Nmcli.disconnectEthernet(modelData.connection, () => {}); - } else { - Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); + StateLayer { + function onClicked(): void { + if (modelData.connected && modelData.connection) { + Nmcli.disconnectEthernet(modelData.connection, () => {}); + } else { + Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); + } } } - } - MaterialIcon { - id: connectIcon + MaterialIcon { + id: connectIcon - anchors.centerIn: parent - text: modelData.connected ? "link_off" : "link" - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + anchors.centerIn: parent + text: modelData.connected ? "link_off" : "link" + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } } } - } - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } } } } - } - - CollapsibleSection { - id: wirelessListSection - Layout.fillWidth: true - title: qsTr("Wireless") - expanded: true + CollapsibleSection { + id: wirelessListSection - ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + title: qsTr("Wireless") + expanded: true - RowLayout { + ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small - StyledText { - text: qsTr("Networks (%1)").arg(Nmcli.networks.length) - font.pointSize: Appearance.font.size.normal - font.weight: 500 + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Networks (%1)").arg(Nmcli.networks.length) + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + StyledText { + visible: Nmcli.scanning + text: qsTr("Scanning...") + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.small + } } StyledText { - visible: Nmcli.scanning - text: qsTr("Scanning...") - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.small + Layout.fillWidth: true + text: qsTr("All available WiFi networks") + color: Colours.palette.m3outline } - } - - StyledText { - Layout.fillWidth: true - text: qsTr("All available WiFi networks") - color: Colours.palette.m3outline - } - Repeater { - id: wirelessRepeater + Repeater { + id: wirelessRepeater - Layout.fillWidth: true - model: ScriptModel { - values: [...Nmcli.networks].sort((a, b) => { - // Put active/connected network first - if (a.active !== b.active) - return b.active - a.active; - // Then sort by signal strength - return b.strength - a.strength; - }) - } + Layout.fillWidth: true + model: ScriptModel { + values: [...Nmcli.networks].sort((a, b) => { + // Put active/connected network first + if (a.active !== b.active) + return b.active - a.active; + // Then sort by signal strength + return b.strength - a.strength; + }) + } - delegate: StyledRect { - required property var modelData + delegate: StyledRect { + required property var modelData - Layout.fillWidth: true + Layout.fillWidth: true - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.network.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (modelData && root.session.network.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal StateLayer { function onClicked(): void { + if (!modelData) { + return; + } root.session.ethernet.active = null; root.session.network.active = modelData; // Check if we need to refresh saved connections when selecting a network - if (modelData && modelData.ssid) { + if (modelData.ssid) { checkSavedProfileForNetwork(modelData.ssid); } } } - RowLayout { - id: wirelessRowLayout + RowLayout { + id: wirelessRowLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledRect { - implicitWidth: implicitHeight - implicitHeight: wirelessIcon.implicitHeight + Appearance.padding.normal * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: wirelessIcon.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.normal - color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.normal + color: (modelData && modelData.active) ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - MaterialIcon { - id: wirelessIcon + MaterialIcon { + id: wirelessIcon - anchors.centerIn: parent - text: Icons.getNetworkIcon(modelData.strength) - font.pointSize: Appearance.font.size.large - fill: modelData.active ? 1 : 0 - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + anchors.centerIn: parent + text: Icons.getNetworkIcon(modelData && modelData.strength !== undefined ? modelData.strength : 0) + font.pointSize: Appearance.font.size.large + fill: (modelData && modelData.active) ? 1 : 0 + color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } } - } - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 - text: modelData.ssid || qsTr("Unknown") - } + text: (modelData && modelData.ssid) ? modelData.ssid : qsTr("Unknown") + } - StyledText { - text: modelData.active ? qsTr("Connected") : (modelData.isSecure ? qsTr("Secured") : qsTr("Open")) - color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: modelData.active ? 500 : 400 - } + StyledText { + text: (modelData && modelData.active) ? qsTr("Connected") : ((modelData && modelData.isSecure) ? qsTr("Secured") : qsTr("Open")) + color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: (modelData && modelData.active) ? 500 : 400 + } - StyledRect { - implicitWidth: implicitHeight - implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2 - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, (modelData && modelData.active) ? 1 : 0) - StateLayer { - function onClicked(): void { - if (modelData.active) { - Nmcli.disconnectFromNetwork(); - } else { - handleWirelessConnect(modelData); + StateLayer { + function onClicked(): void { + if (modelData && modelData.active) { + Nmcli.disconnectFromNetwork(); + } else if (modelData) { + handleWirelessConnect(modelData); + } } } - } - MaterialIcon { - id: wirelessConnectIcon + MaterialIcon { + id: wirelessConnectIcon - anchors.centerIn: parent - text: modelData.active ? "link_off" : "link" - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + anchors.centerIn: parent + text: (modelData && modelData.active) ? "link_off" : "link" + color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } } } - } - implicitHeight: wirelessRowLayout.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: wirelessRowLayout.implicitHeight + Appearance.padding.normal * 2 + } } } } } } - } - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: rightBorder.innerRadius - color: "transparent" - clip: true - - // 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 || "") : "") + Item { + Layout.fillWidth: true + Layout.fillHeight: true + ClippingRectangle { anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - - opacity: 1 - scale: 1 - transformOrigin: Item.Center + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + radius: rightBorder.innerRadius + color: "transparent" clip: true - asynchronous: true - sourceComponent: pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings - - 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 - } - } - PropertyAction {} - ParallelAnimation { - Anim { - target: loader - property: "opacity" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + + // 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 || "") : "") + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + + clip: true + asynchronous: true + sourceComponent: pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings + + 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 + } } - Anim { - target: loader - property: "scale" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + PropertyAction {} + 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 + } } } } - } - onPaneChanged: { - paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + onPaneChanged: { + paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + } } } - } - InnerBorder { - id: rightBorder + InnerBorder { + id: rightBorder - leftThickness: Appearance.padding.normal / 2 - } + leftThickness: Appearance.padding.normal / 2 + } - Component { - id: settings + Component { + id: settings - StyledFlickable { - id: settingsFlickable + StyledFlickable { + id: settingsFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - clip: true + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + clip: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } - NetworkSettings { - id: settingsInner + NetworkSettings { + id: settingsInner - anchors.left: parent.left - anchors.right: parent.right - session: root.session + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } } } - } - Component { - id: ethernetDetails + Component { + id: ethernetDetails - EthernetDetails { - session: root.session + EthernetDetails { + session: root.session + } } - } - Component { - id: wirelessDetails + Component { + id: wirelessDetails - WirelessDetails { - session: root.session + WirelessDetails { + session: root.session + } } } } WirelessPasswordDialog { - Layout.fillWidth: true - Layout.fillHeight: true + anchors.fill: parent session: root.session z: 1000 } diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index f3381b7..4b350be 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -131,14 +131,17 @@ Item { Layout.alignment: Qt.AlignHCenter Layout.topMargin: Appearance.spacing.small - visible: connectButton.connecting + visible: connectButton.connecting || connectButton.hasError text: { + if (connectButton.hasError) { + return qsTr("Connection failed. Please check your password and try again."); + } if (connectButton.connecting) { return qsTr("Connecting..."); } return ""; } - color: Colours.palette.m3onSurfaceVariant + color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small font.weight: 400 wrapMode: Text.WordWrap @@ -153,18 +156,31 @@ Item { focus: true Keys.onPressed: event => { + // Ensure we have focus when receiving keyboard input + if (!activeFocus) { + forceActiveFocus(); + } + + // Clear error when user starts typing + if (connectButton.hasError && event.text && event.text.length > 0) { + connectButton.hasError = false; + } + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { if (connectButton.enabled) { connectButton.clicked(); } + event.accepted = true; } else if (event.key === Qt.Key_Backspace) { if (event.modifiers & Qt.ControlModifier) { passwordBuffer = ""; } else { passwordBuffer = passwordBuffer.slice(0, -1); } + event.accepted = true; } else if (event.text && event.text.length > 0) { passwordBuffer += event.text; + event.accepted = true; } } @@ -178,6 +194,7 @@ Item { Qt.callLater(() => { passwordContainer.forceActiveFocus(); passwordContainer.passwordBuffer = ""; + connectButton.hasError = false; }); } } @@ -198,13 +215,29 @@ Item { StyledRect { anchors.fill: parent radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - border.width: passwordContainer.activeFocus ? 2 : 1 - border.color: passwordContainer.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline + color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer + border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0) + border.color: { + if (connectButton.hasError) { + return Colours.palette.m3error; + } + if (passwordContainer.activeFocus) { + return Colours.palette.m3primary; + } + return root.visible ? Colours.palette.m3outline : "transparent"; + } Behavior on border.color { CAnim {} } + + Behavior on border.width { + CAnim {} + } + + Behavior on color { + CAnim {} + } } StateLayer { @@ -329,6 +362,9 @@ Item { TextButton { id: connectButton + property bool connecting: false + property bool hasError: false + Layout.fillWidth: true Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 inactiveColour: Colours.palette.m3primary @@ -336,8 +372,6 @@ Item { text: qsTr("Connect") enabled: passwordContainer.passwordBuffer.length > 0 && !connecting - property bool connecting: false - onClicked: { if (!root.network || connecting) { return; @@ -348,6 +382,9 @@ Item { return; } + // Clear any previous error + hasError = false; + // Set connecting state connecting = true; enabled = false; @@ -361,11 +398,27 @@ Item { // Shouldn't happen since we provided password connectionMonitor.stop(); connecting = false; + hasError = true; enabled = true; text = qsTr("Connect"); - } else - // Connection failed, monitor will handle timeout - {} + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } else { + // Connection failed immediately - show error + connectionMonitor.stop(); + connecting = false; + hasError = true; + enabled = true; + text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } }); // Start monitoring connection @@ -386,18 +439,8 @@ Item { if (isConnected) { // Successfully connected - give it a moment for network list to update - Qt.callLater(() => { - // Double-check connection is still active - if (root.visible && Nmcli.active && Nmcli.active.ssid) { - const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - if (stillConnected) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - closeDialog(); - } - } - }, 500); + // Use Timer for actual delay + connectionSuccessTimer.start(); return; } @@ -407,8 +450,14 @@ Item { if (connectionMonitor.repeatCount > 10) { connectionMonitor.stop(); connectButton.connecting = false; + connectButton.hasError = true; connectButton.enabled = true; connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } } } } @@ -432,6 +481,23 @@ Item { } } + Timer { + id: connectionSuccessTimer + interval: 500 + onTriggered: { + // Double-check connection is still active + if (root.visible && Nmcli.active && Nmcli.active.ssid) { + const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + closeDialog(); + } + } + } + } + Connections { target: Nmcli function onActiveChanged() { @@ -443,8 +509,12 @@ Item { if (root.visible && root.network && root.network.ssid === ssid && connectButton.connecting) { connectionMonitor.stop(); connectButton.connecting = false; + connectButton.hasError = true; connectButton.enabled = true; connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + Nmcli.forgetNetwork(ssid); } } } @@ -457,6 +527,7 @@ Item { isClosing = true; passwordContainer.passwordBuffer = ""; connectButton.connecting = false; + connectButton.hasError = false; connectButton.text = qsTr("Connect"); connectionMonitor.stop(); } diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 24a93da..36bd3e6 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -1272,7 +1272,15 @@ Singleton { stdout: SplitParser { onRead: root.refreshOnConnectionChange() } - onExited: Qt.callLater(() => monitorProc.running = true, 2000) + onExited: monitorRestartTimer.start() + } + + Timer { + id: monitorRestartTimer + interval: 2000 + onTriggered: { + monitorProc.running = true; + } } function refreshOnConnectionChange(): void { -- cgit v1.2.3-freya From f29f0a3805239976778c1492cf846d4f614e660c Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 01:36:57 -0500 Subject: controlcenter: removed FileView and now all use singleton Config --- .../controlcenter/appearance/AppearancePane.qml | 225 +++++---------------- modules/controlcenter/launcher/LauncherPane.qml | 77 +++---- modules/controlcenter/taskbar/TaskbarPane.qml | 196 ++++++------------ 3 files changed, 138 insertions(+), 360 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index fd75ff4..6f77d25 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -12,7 +12,6 @@ import qs.config import qs.utils import Caelestia.Models import Quickshell -import Quickshell.Io import QtQuick import QtQuick.Layouts @@ -22,117 +21,32 @@ RowLayout { required property Session session // Appearance settings - property real animDurationsScale: 1 - property string fontFamilyMaterial: "Material Symbols Rounded" - property string fontFamilyMono: "CaskaydiaCove NF" - property string fontFamilySans: "Rubik" - property real fontSizeScale: 1 - property real paddingScale: 1 - property real roundingScale: 1 - property real spacingScale: 1 - property bool transparencyEnabled: false - property real transparencyBase: 0.85 - property real transparencyLayers: 0.4 - property real borderRounding: 1 - property real borderThickness: 1 + property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1 + property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded" + property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF" + property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik" + property real fontSizeScale: Config.appearance.font.size.scale ?? 1 + property real paddingScale: Config.appearance.padding.scale ?? 1 + property real roundingScale: Config.appearance.rounding.scale ?? 1 + property real spacingScale: Config.appearance.spacing.scale ?? 1 + property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false + property real transparencyBase: Config.appearance.transparency.base ?? 0.85 + property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4 + property real borderRounding: Config.border.rounding ?? 1 + property real borderThickness: Config.border.thickness ?? 1 // Background settings - property bool desktopClockEnabled: true - property bool backgroundEnabled: true - property bool visualiserEnabled: true - property bool visualiserAutoHide: true - property real visualiserRounding: 1 - property real visualiserSpacing: 1 + property bool desktopClockEnabled: Config.background.desktopClock.enabled ?? false + property bool backgroundEnabled: Config.background.enabled ?? true + property bool visualiserEnabled: Config.background.visualiser.enabled ?? false + property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true + property real visualiserRounding: Config.background.visualiser.rounding ?? 1 + property real visualiserSpacing: Config.background.visualiser.spacing ?? 1 anchors.fill: parent spacing: 0 - FileView { - id: configFile - - path: `${Paths.config}/shell.json` - watchChanges: true - - onLoaded: { - try { - const config = JSON.parse(text()); - updateFromConfig(config); - } catch (e) { - console.error("Failed to parse config:", e); - } - } - - onSaveFailed: err => { - console.error("Failed to save config file:", err); - } - } - - function updateFromConfig(config) { - // Update appearance settings - if (config.appearance) { - if (config.appearance.anim && config.appearance.anim.durations) { - root.animDurationsScale = config.appearance.anim.durations.scale ?? 1; - } - if (config.appearance.font) { - if (config.appearance.font.family) { - root.fontFamilyMaterial = config.appearance.font.family.material ?? "Material Symbols Rounded"; - root.fontFamilyMono = config.appearance.font.family.mono ?? "CaskaydiaCove NF"; - root.fontFamilySans = config.appearance.font.family.sans ?? "Rubik"; - } - if (config.appearance.font.size) { - root.fontSizeScale = config.appearance.font.size.scale ?? 1; - } - } - if (config.appearance.padding) { - root.paddingScale = config.appearance.padding.scale ?? 1; - } - if (config.appearance.rounding) { - root.roundingScale = config.appearance.rounding.scale ?? 1; - } - if (config.appearance.spacing) { - root.spacingScale = config.appearance.spacing.scale ?? 1; - } - if (config.appearance.transparency) { - root.transparencyEnabled = config.appearance.transparency.enabled ?? false; - root.transparencyBase = config.appearance.transparency.base ?? 0.85; - root.transparencyLayers = config.appearance.transparency.layers ?? 0.4; - } - } - - // Update border settings - if (config.border) { - root.borderRounding = config.border.rounding ?? 1; - root.borderThickness = config.border.thickness ?? 1; - } - - // Update background settings - if (config.background) { - root.desktopClockEnabled = config.background.desktopClock?.enabled !== undefined ? config.background.desktopClock.enabled : false; - root.backgroundEnabled = config.background.enabled !== undefined ? config.background.enabled : true; - if (config.background.visualiser) { - root.visualiserEnabled = config.background.visualiser.enabled !== undefined ? config.background.visualiser.enabled : false; - root.visualiserAutoHide = config.background.visualiser.autoHide !== undefined ? config.background.visualiser.autoHide : true; - root.visualiserRounding = config.background.visualiser.rounding !== undefined ? config.background.visualiser.rounding : 1; - root.visualiserSpacing = config.background.visualiser.spacing !== undefined ? config.background.visualiser.spacing : 1; - } else { - // Set defaults if visualiser object doesn't exist (matching BackgroundConfig defaults) - root.visualiserEnabled = false; - root.visualiserAutoHide = true; - root.visualiserRounding = 1; - root.visualiserSpacing = 1; - } - } else { - // Set defaults if background object doesn't exist (matching BackgroundConfig defaults) - root.desktopClockEnabled = false; - root.backgroundEnabled = true; - root.visualiserEnabled = false; - root.visualiserAutoHide = true; - root.visualiserRounding = 1; - root.visualiserSpacing = 1; - } - } - function collapseAllSections(exceptSection) { if (exceptSection !== themeModeSection) themeModeSection.expanded = false; if (exceptSection !== colorVariantSection) colorVariantSection.expanded = false; @@ -146,73 +60,40 @@ RowLayout { } function saveConfig() { - if (!configFile.loaded) { - console.error("Config file not loaded yet"); - return; - } - - try { - const config = JSON.parse(configFile.text()); - - // Ensure appearance object exists - if (!config.appearance) config.appearance = {}; - - // Update animations - if (!config.appearance.anim) config.appearance.anim = {}; - if (!config.appearance.anim.durations) config.appearance.anim.durations = {}; - config.appearance.anim.durations.scale = root.animDurationsScale; - - // Update fonts - if (!config.appearance.font) config.appearance.font = {}; - if (!config.appearance.font.family) config.appearance.font.family = {}; - config.appearance.font.family.material = root.fontFamilyMaterial; - config.appearance.font.family.mono = root.fontFamilyMono; - config.appearance.font.family.sans = root.fontFamilySans; - if (!config.appearance.font.size) config.appearance.font.size = {}; - config.appearance.font.size.scale = root.fontSizeScale; - - // Update scales - if (!config.appearance.padding) config.appearance.padding = {}; - config.appearance.padding.scale = root.paddingScale; - if (!config.appearance.rounding) config.appearance.rounding = {}; - config.appearance.rounding.scale = root.roundingScale; - if (!config.appearance.spacing) config.appearance.spacing = {}; - config.appearance.spacing.scale = root.spacingScale; - - // Update transparency - if (!config.appearance.transparency) config.appearance.transparency = {}; - config.appearance.transparency.enabled = root.transparencyEnabled; - config.appearance.transparency.base = root.transparencyBase; - config.appearance.transparency.layers = root.transparencyLayers; - - // Ensure background object exists - if (!config.background) config.background = {}; - - // Update desktop clock - if (!config.background.desktopClock) config.background.desktopClock = {}; - config.background.desktopClock.enabled = root.desktopClockEnabled; - - // Update background enabled - config.background.enabled = root.backgroundEnabled; - - // Update visualiser - if (!config.background.visualiser) config.background.visualiser = {}; - config.background.visualiser.enabled = root.visualiserEnabled; - config.background.visualiser.autoHide = root.visualiserAutoHide; - config.background.visualiser.rounding = root.visualiserRounding; - config.background.visualiser.spacing = root.visualiserSpacing; - - // Update border - if (!config.border) config.border = {}; - config.border.rounding = root.borderRounding; - config.border.thickness = root.borderThickness; - - // Write back to file using setText (same simple approach that worked for taskbar) - const jsonString = JSON.stringify(config, null, 4); - configFile.setText(jsonString); - } catch (e) { - console.error("Failed to save config:", e); - } + // Update animations + Config.appearance.anim.durations.scale = root.animDurationsScale; + + // Update fonts + Config.appearance.font.family.material = root.fontFamilyMaterial; + Config.appearance.font.family.mono = root.fontFamilyMono; + Config.appearance.font.family.sans = root.fontFamilySans; + Config.appearance.font.size.scale = root.fontSizeScale; + + // Update scales + Config.appearance.padding.scale = root.paddingScale; + Config.appearance.rounding.scale = root.roundingScale; + Config.appearance.spacing.scale = root.spacingScale; + + // Update transparency + Config.appearance.transparency.enabled = root.transparencyEnabled; + Config.appearance.transparency.base = root.transparencyBase; + Config.appearance.transparency.layers = root.transparencyLayers; + + // Update desktop clock + Config.background.desktopClock.enabled = root.desktopClockEnabled; + + // Update background enabled + Config.background.enabled = root.backgroundEnabled; + + // Update visualiser + Config.background.visualiser.enabled = root.visualiserEnabled; + Config.background.visualiser.autoHide = root.visualiserAutoHide; + Config.background.visualiser.rounding = root.visualiserRounding; + Config.background.visualiser.spacing = root.visualiserSpacing; + + // Update border + Config.border.rounding = root.borderRounding; + Config.border.thickness = root.borderThickness; } Item { diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 0a1175f..d585c32 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -11,7 +11,6 @@ import qs.config import qs.utils import Caelestia import Quickshell -import Quickshell.Io import Quickshell.Widgets import QtQuick import QtQuick.Layouts @@ -29,74 +28,46 @@ RowLayout { spacing: 0 - FileView { - id: configFile - - path: `${Paths.config}/shell.json` - watchChanges: true - - onLoaded: { - try { - const config = JSON.parse(text()); - updateToggleState(); - } catch (e) { - console.error("Failed to parse config:", e); - } - } - } - function updateToggleState() { - if (!root.selectedApp || !configFile.loaded) { + if (!root.selectedApp) { root.hideFromLauncherChecked = false; return; } - try { - const config = JSON.parse(configFile.text()); - const appId = root.selectedApp.id || root.selectedApp.entry?.id; + const appId = root.selectedApp.id || root.selectedApp.entry?.id; - if (config.launcher && config.launcher.hiddenApps) { - root.hideFromLauncherChecked = config.launcher.hiddenApps.includes(appId); - } else { - root.hideFromLauncherChecked = false; - } - } catch (e) { - console.error("Failed to update toggle state:", e); + if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { + root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); + } else { + root.hideFromLauncherChecked = false; } } function saveHiddenApps(isHidden) { - if (!configFile.loaded || !root.selectedApp) { + if (!root.selectedApp) { return; } - try { - const config = JSON.parse(configFile.text()); - const appId = root.selectedApp.id || root.selectedApp.entry?.id; - - if (!config.launcher) config.launcher = {}; - if (!config.launcher.hiddenApps) config.launcher.hiddenApps = []; + const appId = root.selectedApp.id || root.selectedApp.entry?.id; - const hiddenApps = config.launcher.hiddenApps; + // Create a new array to ensure change detection + const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; - if (isHidden) { - // Add to hiddenApps if not already there - if (!hiddenApps.includes(appId)) { - hiddenApps.push(appId); - } - } else { - // Remove from hiddenApps - const index = hiddenApps.indexOf(appId); - if (index !== -1) { - hiddenApps.splice(index, 1); - } + if (isHidden) { + // Add to hiddenApps if not already there + if (!hiddenApps.includes(appId)) { + hiddenApps.push(appId); + } + } else { + // Remove from hiddenApps + const index = hiddenApps.indexOf(appId); + if (index !== -1) { + hiddenApps.splice(index, 1); } - - const jsonString = JSON.stringify(config, null, 4); - configFile.setText(jsonString); - } catch (e) { - console.error("Failed to save config:", e); } + + // Update Config to trigger save + Config.launcher.hiddenApps = hiddenApps; } onSelectedAppChanged: { @@ -437,7 +408,7 @@ RowLayout { visible: root.selectedApp !== null label: qsTr("Hide from launcher") checked: root.hideFromLauncherChecked - enabled: root.selectedApp !== null && configFile.loaded + enabled: root.selectedApp !== null onToggled: checked => { root.hideFromLauncherChecked = checked; root.saveHiddenApps(checked); diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 72e9a80..5385ab7 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -9,7 +9,6 @@ import qs.services import qs.config import qs.utils import Quickshell -import Quickshell.Io import QtQuick import QtQuick.Layouts @@ -19,171 +18,98 @@ RowLayout { required property Session session // Bar Behavior - property bool persistent: true - property bool showOnHover: true - property int dragThreshold: 20 + property bool persistent: Config.bar.persistent ?? true + property bool showOnHover: Config.bar.showOnHover ?? true + property int dragThreshold: Config.bar.dragThreshold ?? 20 // Status Icons - property bool showAudio: true - property bool showMicrophone: true - property bool showKbLayout: false - property bool showNetwork: true - property bool showBluetooth: true - property bool showBattery: true - property bool showLockStatus: true + property bool showAudio: Config.bar.status.showAudio ?? true + property bool showMicrophone: Config.bar.status.showMicrophone ?? true + property bool showKbLayout: Config.bar.status.showKbLayout ?? false + property bool showNetwork: Config.bar.status.showNetwork ?? true + property bool showBluetooth: Config.bar.status.showBluetooth ?? true + property bool showBattery: Config.bar.status.showBattery ?? true + property bool showLockStatus: Config.bar.status.showLockStatus ?? true // Tray Settings - property bool trayBackground: false - property bool trayCompact: false - property bool trayRecolour: false + property bool trayBackground: Config.bar.tray.background ?? false + property bool trayCompact: Config.bar.tray.compact ?? false + property bool trayRecolour: Config.bar.tray.recolour ?? false // Workspaces - property int workspacesShown: 5 - property bool workspacesActiveIndicator: true - property bool workspacesOccupiedBg: false - property bool workspacesShowWindows: false - property bool workspacesPerMonitor: true + property int workspacesShown: Config.bar.workspaces.shown ?? 5 + property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true + property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false + property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false + property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true anchors.fill: parent spacing: 0 - FileView { - id: configFile - - path: `${Paths.config}/shell.json` - watchChanges: true - - onLoaded: { - try { - const config = JSON.parse(text()); - updateFromConfig(config); - } catch (e) { - console.error("Failed to parse config:", e); - } - } - } - - function updateFromConfig(config) { + Component.onCompleted: { // Update clock toggle - if (config.bar && config.bar.clock) { - clockShowIconSwitch.checked = config.bar.clock.showIcon !== false; - } + clockShowIconSwitch.checked = Config.bar.clock.showIcon ?? true; // Update entries - if (config.bar && config.bar.entries) { + if (Config.bar.entries) { entriesModel.clear(); - for (const entry of config.bar.entries) { + for (let i = 0; i < Config.bar.entries.length; i++) { + const entry = Config.bar.entries[i]; entriesModel.append({ id: entry.id, enabled: entry.enabled !== false }); } } + } + + function saveConfig(entryIndex, entryEnabled) { + // Update clock setting + Config.bar.clock.showIcon = clockShowIconSwitch.checked; // Update bar behavior - if (config.bar) { - root.persistent = config.bar.persistent !== false; - root.showOnHover = config.bar.showOnHover !== false; - root.dragThreshold = config.bar.dragThreshold || 20; - } + Config.bar.persistent = root.persistent; + Config.bar.showOnHover = root.showOnHover; + Config.bar.dragThreshold = root.dragThreshold; // Update status icons - if (config.bar && config.bar.status) { - root.showAudio = config.bar.status.showAudio !== false; - root.showMicrophone = config.bar.status.showMicrophone !== false; - root.showKbLayout = config.bar.status.showKbLayout === true; - root.showNetwork = config.bar.status.showNetwork !== false; - root.showBluetooth = config.bar.status.showBluetooth !== false; - root.showBattery = config.bar.status.showBattery !== false; - root.showLockStatus = config.bar.status.showLockStatus !== false; - } + Config.bar.status.showAudio = root.showAudio; + Config.bar.status.showMicrophone = root.showMicrophone; + Config.bar.status.showKbLayout = root.showKbLayout; + Config.bar.status.showNetwork = root.showNetwork; + Config.bar.status.showBluetooth = root.showBluetooth; + Config.bar.status.showBattery = root.showBattery; + Config.bar.status.showLockStatus = root.showLockStatus; // Update tray settings - if (config.bar && config.bar.tray) { - root.trayBackground = config.bar.tray.background === true; - root.trayCompact = config.bar.tray.compact === true; - root.trayRecolour = config.bar.tray.recolour === true; - } + Config.bar.tray.background = root.trayBackground; + Config.bar.tray.compact = root.trayCompact; + Config.bar.tray.recolour = root.trayRecolour; // Update workspaces - if (config.bar && config.bar.workspaces) { - root.workspacesShown = config.bar.workspaces.shown || 5; - root.workspacesActiveIndicator = config.bar.workspaces.activeIndicator !== false; - root.workspacesOccupiedBg = config.bar.workspaces.occupiedBg === true; - root.workspacesShowWindows = config.bar.workspaces.showWindows === true; - root.workspacesPerMonitor = config.bar.workspaces.perMonitorWorkspaces !== false; - } - } - - function saveConfig(entryIndex, entryEnabled) { - if (!configFile.loaded) { - return; - } - - try { - const config = JSON.parse(configFile.text()); - - // Ensure bar object exists - if (!config.bar) config.bar = {}; - - // Update clock setting - if (!config.bar.clock) config.bar.clock = {}; - config.bar.clock.showIcon = clockShowIconSwitch.checked; - - // Update bar behavior - config.bar.persistent = root.persistent; - config.bar.showOnHover = root.showOnHover; - config.bar.dragThreshold = root.dragThreshold; - - // Update status icons - if (!config.bar.status) config.bar.status = {}; - config.bar.status.showAudio = root.showAudio; - config.bar.status.showMicrophone = root.showMicrophone; - config.bar.status.showKbLayout = root.showKbLayout; - config.bar.status.showNetwork = root.showNetwork; - config.bar.status.showBluetooth = root.showBluetooth; - config.bar.status.showBattery = root.showBattery; - config.bar.status.showLockStatus = root.showLockStatus; - - // Update tray settings - if (!config.bar.tray) config.bar.tray = {}; - config.bar.tray.background = root.trayBackground; - config.bar.tray.compact = root.trayCompact; - config.bar.tray.recolour = root.trayRecolour; - - // Update workspaces - if (!config.bar.workspaces) config.bar.workspaces = {}; - config.bar.workspaces.shown = root.workspacesShown; - config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; - config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; - config.bar.workspaces.showWindows = root.workspacesShowWindows; - config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; - - // Update entries from the model (same approach as clock - use provided value if available) - if (!config.bar.entries) config.bar.entries = []; - config.bar.entries = []; - - for (let i = 0; i < entriesModel.count; i++) { - const entry = entriesModel.get(i); - // If this is the entry being updated, use the provided value (same as clock toggle reads from switch) - // Otherwise use the value from the model - let enabled = entry.enabled; - if (entryIndex !== undefined && i === entryIndex) { - enabled = entryEnabled; - } - config.bar.entries.push({ - id: entry.id, - enabled: enabled - }); + Config.bar.workspaces.shown = root.workspacesShown; + Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; + Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; + Config.bar.workspaces.showWindows = root.workspacesShowWindows; + Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; + + // Update entries from the model (same approach as clock - use provided value if available) + const entries = []; + for (let i = 0; i < entriesModel.count; i++) { + const entry = entriesModel.get(i); + // If this is the entry being updated, use the provided value (same as clock toggle reads from switch) + // Otherwise use the value from the model + let enabled = entry.enabled; + if (entryIndex !== undefined && i === entryIndex) { + enabled = entryEnabled; } - - // Write back to file using setText (same simple approach that worked for clock) - const jsonString = JSON.stringify(config, null, 4); - configFile.setText(jsonString); - } catch (e) { - console.error("Failed to save config:", e); + entries.push({ + id: entry.id, + enabled: enabled + }); } + Config.bar.entries = entries; } ListModel { -- cgit v1.2.3-freya From 56ed76e183483e36c42d1a2e2fbca79b77f0cb1e Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 01:39:41 -0500 Subject: controlcenter: changed mute button icons --- modules/controlcenter/audio/AudioPane.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 32d6680..b4a0bf1 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -304,7 +304,7 @@ RowLayout { id: muteIcon anchors.centerIn: parent - text: Audio.muted ? "volume_off" : "volume_up" + text: Audio.muted ? "volume_off" : "volume_mute" color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer } } @@ -374,7 +374,7 @@ RowLayout { id: muteInputIcon anchors.centerIn: parent - text: Audio.sourceMuted ? "mic_off" : "mic" + text: "mic_off" color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer } } -- cgit v1.2.3-freya From 266cffe3421b4619ee10205a20058ab4c52def9e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:43:03 +1100 Subject: controlcenter: fix wallpaper gradient overlay --- .../controlcenter/appearance/AppearancePane.qml | 1184 ++++++++++---------- 1 file changed, 596 insertions(+), 588 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 6f77d25..1e81205 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -48,15 +48,24 @@ RowLayout { spacing: 0 function collapseAllSections(exceptSection) { - if (exceptSection !== themeModeSection) themeModeSection.expanded = false; - if (exceptSection !== colorVariantSection) colorVariantSection.expanded = false; - if (exceptSection !== colorSchemeSection) colorSchemeSection.expanded = false; - if (exceptSection !== animationsSection) animationsSection.expanded = false; - if (exceptSection !== fontsSection) fontsSection.expanded = false; - if (exceptSection !== scalesSection) scalesSection.expanded = false; - if (exceptSection !== transparencySection) transparencySection.expanded = false; - if (exceptSection !== borderSection) borderSection.expanded = false; - if (exceptSection !== backgroundSection) backgroundSection.expanded = false; + if (exceptSection !== themeModeSection) + themeModeSection.expanded = false; + if (exceptSection !== colorVariantSection) + colorVariantSection.expanded = false; + if (exceptSection !== colorSchemeSection) + colorSchemeSection.expanded = false; + if (exceptSection !== animationsSection) + animationsSection.expanded = false; + if (exceptSection !== fontsSection) + fontsSection.expanded = false; + if (exceptSection !== scalesSection) + scalesSection.expanded = false; + if (exceptSection !== transparencySection) + transparencySection.expanded = false; + if (exceptSection !== borderSection) + borderSection.expanded = false; + if (exceptSection !== backgroundSection) + backgroundSection.expanded = false; } function saveConfig() { @@ -123,712 +132,712 @@ RowLayout { spacing: Appearance.spacing.small - RowLayout { - spacing: Appearance.spacing.smaller + RowLayout { + spacing: Appearance.spacing.smaller - StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } - Item { - Layout.fillWidth: true + Item { + Layout.fillWidth: true + } } - } - CollapsibleSection { - id: themeModeSection - title: qsTr("Theme mode") - description: qsTr("Light or dark theme") - onToggleRequested: { - root.collapseAllSections(themeModeSection); - } + CollapsibleSection { + id: themeModeSection + title: qsTr("Theme mode") + description: qsTr("Light or dark theme") + onToggleRequested: { + root.collapseAllSections(themeModeSection); + } - SwitchRow { - label: qsTr("Dark mode") - checked: !Colours.currentLight - onToggled: checked => { - Colours.setMode(checked ? "dark" : "light"); + SwitchRow { + label: qsTr("Dark mode") + checked: !Colours.currentLight + onToggled: checked => { + Colours.setMode(checked ? "dark" : "light"); + } } } - } - CollapsibleSection { - id: colorVariantSection - title: qsTr("Color variant") - description: qsTr("Material theme variant") - onToggleRequested: { - root.collapseAllSections(colorVariantSection); - } + CollapsibleSection { + id: colorVariantSection + title: qsTr("Color variant") + description: qsTr("Material theme variant") + onToggleRequested: { + root.collapseAllSections(colorVariantSection); + } - StyledListView { - Layout.fillWidth: true - implicitHeight: colorVariantSection.expanded ? Math.min(400, M3Variants.list.length * 60) : 0 + StyledListView { + Layout.fillWidth: true + implicitHeight: colorVariantSection.expanded ? Math.min(400, M3Variants.list.length * 60) : 0 - model: M3Variants.list - spacing: Appearance.spacing.small / 2 - clip: true + model: M3Variants.list + spacing: Appearance.spacing.small / 2 + clip: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } - delegate: StyledRect { - required property var modelData + delegate: StyledRect { + required property var modelData - width: parent ? parent.width : 0 + width: parent ? parent.width : 0 - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 - border.color: Colours.palette.m3primary + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 + border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - const variant = modelData.variant; + StateLayer { + function onClicked(): void { + const variant = modelData.variant; - // Optimistic update - set immediately - Schemes.currentVariant = variant; + // Optimistic update - set immediately + Schemes.currentVariant = variant; - // Execute the command - Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); + // Execute the command + Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); - // Reload after a delay to confirm - Qt.callLater(() => { - reloadTimer.restart(); - }); + // Reload after a delay to confirm + Qt.callLater(() => { + reloadTimer.restart(); + }); + } } - } - Timer { - id: reloadTimer - interval: 300 - onTriggered: { - Schemes.reload(); + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); + } } - } - RowLayout { - id: variantRow + RowLayout { + id: variantRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - MaterialIcon { - text: modelData.icon - font.pointSize: Appearance.font.size.large - fill: modelData.variant === Schemes.currentVariant ? 1 : 0 - } + MaterialIcon { + text: modelData.icon + font.pointSize: Appearance.font.size.large + fill: modelData.variant === Schemes.currentVariant ? 1 : 0 + } - StyledText { - Layout.fillWidth: true - text: modelData.name - font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 - } + StyledText { + Layout.fillWidth: true + text: modelData.name + font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 + } - MaterialIcon { - visible: modelData.variant === Schemes.currentVariant - text: "check" - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large + MaterialIcon { + visible: modelData.variant === Schemes.currentVariant + text: "check" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.large + } } - } - implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 + } } } - } - CollapsibleSection { - id: colorSchemeSection - title: qsTr("Color scheme") - description: qsTr("Available color schemes") - onToggleRequested: { - root.collapseAllSections(colorSchemeSection); - } + CollapsibleSection { + id: colorSchemeSection + title: qsTr("Color scheme") + description: qsTr("Available color schemes") + onToggleRequested: { + root.collapseAllSections(colorSchemeSection); + } - StyledListView { - Layout.fillWidth: true - implicitHeight: colorSchemeSection.expanded ? Math.min(400, Schemes.list.length * 80) : 0 + StyledListView { + Layout.fillWidth: true + implicitHeight: colorSchemeSection.expanded ? Math.min(400, Schemes.list.length * 80) : 0 - model: Schemes.list - spacing: Appearance.spacing.small / 2 - clip: true + model: Schemes.list + spacing: Appearance.spacing.small / 2 + clip: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } - delegate: StyledRect { - required property var modelData + delegate: StyledRect { + required property var modelData - width: parent ? parent.width : 0 + width: parent ? parent.width : 0 - readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` - readonly property bool isCurrent: schemeKey === Schemes.currentScheme + readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` + readonly property bool isCurrent: schemeKey === Schemes.currentScheme - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - const name = modelData.name; - const flavour = modelData.flavour; - const schemeKey = `${name} ${flavour}`; + StateLayer { + function onClicked(): void { + const name = modelData.name; + const flavour = modelData.flavour; + const schemeKey = `${name} ${flavour}`; - // Optimistic update - set immediately - Schemes.currentScheme = schemeKey; + // Optimistic update - set immediately + Schemes.currentScheme = schemeKey; - // Execute the command - Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); + // Execute the command + Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - // Reload after a delay to confirm - Qt.callLater(() => { - reloadTimer.restart(); - }); + // Reload after a delay to confirm + Qt.callLater(() => { + reloadTimer.restart(); + }); + } } - } - Timer { - id: reloadTimer - interval: 300 - onTriggered: { - Schemes.reload(); + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); + } } - } - - RowLayout { - id: schemeRow - - anchors.fill: parent - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + RowLayout { + id: schemeRow - StyledRect { - id: preview + anchors.fill: parent + anchors.margins: Appearance.padding.normal - Layout.alignment: Qt.AlignVCenter + spacing: Appearance.spacing.normal - border.width: 1 - border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) + StyledRect { + id: preview - color: `#${modelData.colours?.surface}` - radius: Appearance.rounding.full - implicitWidth: iconPlaceholder.implicitWidth - implicitHeight: iconPlaceholder.implicitWidth + Layout.alignment: Qt.AlignVCenter - MaterialIcon { - id: iconPlaceholder - visible: false - text: "circle" - font.pointSize: Appearance.font.size.large - } + border.width: 1 + border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) - Item { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right + color: `#${modelData.colours?.surface}` + radius: Appearance.rounding.full + implicitWidth: iconPlaceholder.implicitWidth + implicitHeight: iconPlaceholder.implicitWidth - implicitWidth: parent.implicitWidth / 2 - clip: true + MaterialIcon { + id: iconPlaceholder + visible: false + text: "circle" + font.pointSize: Appearance.font.size.large + } - StyledRect { + Item { anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right - implicitWidth: preview.implicitWidth - color: `#${modelData.colours?.primary}` - radius: Appearance.rounding.full + implicitWidth: parent.implicitWidth / 2 + clip: true + + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + implicitWidth: preview.implicitWidth + color: `#${modelData.colours?.primary}` + radius: Appearance.rounding.full + } } } - } - Column { - Layout.fillWidth: true - spacing: 0 + Column { + Layout.fillWidth: true + spacing: 0 - StyledText { - text: modelData.flavour ?? "" - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData.flavour ?? "" + font.pointSize: Appearance.font.size.normal + } - StyledText { - text: modelData.name ?? "" - font.pointSize: Appearance.font.size.small - color: Colours.palette.m3outline + StyledText { + text: modelData.name ?? "" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline - elide: Text.ElideRight - anchors.left: parent.left - anchors.right: parent.right + elide: Text.ElideRight + anchors.left: parent.left + anchors.right: parent.right + } } - } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 + } } } - } - - CollapsibleSection { - id: animationsSection - title: qsTr("Animations") - onToggleRequested: { - root.collapseAllSections(animationsSection); - } - SpinBoxRow { - label: qsTr("Animation duration scale") - min: 0.1 - max: 5 - value: root.animDurationsScale - onValueModified: value => { - root.animDurationsScale = value; - root.saveConfig(); + CollapsibleSection { + id: animationsSection + title: qsTr("Animations") + onToggleRequested: { + root.collapseAllSections(animationsSection); } - } - } - - CollapsibleSection { - id: fontsSection - title: qsTr("Fonts") - onToggleRequested: { - root.collapseAllSections(fontsSection); - } - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Material font family") - font.pointSize: Appearance.font.size.larger - font.weight: 500 + SpinBoxRow { + label: qsTr("Animation duration scale") + min: 0.1 + max: 5 + value: root.animDurationsScale + onValueModified: value => { + root.animDurationsScale = value; + root.saveConfig(); + } + } } - StyledListView { - Layout.fillWidth: true - implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - - model: Qt.fontFamilies() - spacing: Appearance.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent + CollapsibleSection { + id: fontsSection + title: qsTr("Fonts") + onToggleRequested: { + root.collapseAllSections(fontsSection); } - delegate: StyledRect { - required property string modelData + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Material font family") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } - anchors.left: parent.left - anchors.right: parent.right + StyledListView { + Layout.fillWidth: true + implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - readonly property bool isCurrent: modelData === root.fontFamilyMaterial - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + model: Qt.fontFamilies() + spacing: Appearance.spacing.small / 2 + clip: true - StateLayer { - function onClicked(): void { - root.fontFamilyMaterial = modelData; - root.saveConfig(); - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent } - RowLayout { - id: fontFamilyMaterialRow + delegate: StyledRect { + required property string modelData anchors.left: parent.left anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + readonly property bool isCurrent: modelData === root.fontFamilyMaterial + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal + StateLayer { + function onClicked(): void { + root.fontFamilyMaterial = modelData; + root.saveConfig(); + } } - Item { - Layout.fillWidth: true - } + RowLayout { + id: fontFamilyMaterialRow - Loader { - active: isCurrent - asynchronous: true + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } - } - } + spacing: Appearance.spacing.normal - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 - } - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Monospace font family") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + Item { + Layout.fillWidth: true + } - StyledListView { - Layout.fillWidth: true - implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 + Loader { + active: isCurrent + asynchronous: true - model: Qt.fontFamilies() - spacing: Appearance.spacing.small / 2 - clip: true + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + } } - delegate: StyledRect { - required property string modelData + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Monospace font family") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } - anchors.left: parent.left - anchors.right: parent.right + StyledListView { + Layout.fillWidth: true + implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - readonly property bool isCurrent: modelData === root.fontFamilyMono - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + model: Qt.fontFamilies() + spacing: Appearance.spacing.small / 2 + clip: true - StateLayer { - function onClicked(): void { - root.fontFamilyMono = modelData; - root.saveConfig(); - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent } - RowLayout { - id: fontFamilyMonoRow + delegate: StyledRect { + required property string modelData anchors.left: parent.left anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + readonly property bool isCurrent: modelData === root.fontFamilyMono + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal + StateLayer { + function onClicked(): void { + root.fontFamilyMono = modelData; + root.saveConfig(); + } } - Item { - Layout.fillWidth: true - } + RowLayout { + id: fontFamilyMonoRow - Loader { - active: isCurrent - asynchronous: true + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } - } - } + spacing: Appearance.spacing.normal - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 - } - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Sans-serif font family") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + Item { + Layout.fillWidth: true + } - StyledListView { - Layout.fillWidth: true - implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 + Loader { + active: isCurrent + asynchronous: true - model: Qt.fontFamilies() - spacing: Appearance.spacing.small / 2 - clip: true + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + } } - delegate: StyledRect { - required property string modelData + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Sans-serif font family") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } - anchors.left: parent.left - anchors.right: parent.right + StyledListView { + Layout.fillWidth: true + implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - readonly property bool isCurrent: modelData === root.fontFamilySans - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + model: Qt.fontFamilies() + spacing: Appearance.spacing.small / 2 + clip: true - StateLayer { - function onClicked(): void { - root.fontFamilySans = modelData; - root.saveConfig(); - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent } - RowLayout { - id: fontFamilySansRow + delegate: StyledRect { + required property string modelData anchors.left: parent.left anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + readonly property bool isCurrent: modelData === root.fontFamilySans + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal + StateLayer { + function onClicked(): void { + root.fontFamilySans = modelData; + root.saveConfig(); + } } - Item { - Layout.fillWidth: true - } + RowLayout { + id: fontFamilySansRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - Loader { - active: isCurrent - asynchronous: true + spacing: Appearance.spacing.normal - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + } } - } - SpinBoxRow { - label: qsTr("Font size scale") - min: 0.1 - max: 5 - value: root.fontSizeScale - onValueModified: value => { - root.fontSizeScale = value; - root.saveConfig(); + SpinBoxRow { + label: qsTr("Font size scale") + min: 0.1 + max: 5 + value: root.fontSizeScale + onValueModified: value => { + root.fontSizeScale = value; + root.saveConfig(); + } } } - } - CollapsibleSection { - id: scalesSection - title: qsTr("Scales") - onToggleRequested: { - root.collapseAllSections(scalesSection); - } + CollapsibleSection { + id: scalesSection + title: qsTr("Scales") + onToggleRequested: { + root.collapseAllSections(scalesSection); + } - SpinBoxRow { - label: qsTr("Padding scale") - min: 0.1 - max: 5 - value: root.paddingScale - onValueModified: value => { - root.paddingScale = value; - root.saveConfig(); + SpinBoxRow { + label: qsTr("Padding scale") + min: 0.1 + max: 5 + value: root.paddingScale + onValueModified: value => { + root.paddingScale = value; + root.saveConfig(); + } } - } - SpinBoxRow { - label: qsTr("Rounding scale") - min: 0.1 - max: 5 - value: root.roundingScale - onValueModified: value => { - root.roundingScale = value; - root.saveConfig(); + SpinBoxRow { + label: qsTr("Rounding scale") + min: 0.1 + max: 5 + value: root.roundingScale + onValueModified: value => { + root.roundingScale = value; + root.saveConfig(); + } } - } - SpinBoxRow { - label: qsTr("Spacing scale") - min: 0.1 - max: 5 - value: root.spacingScale - onValueModified: value => { - root.spacingScale = value; - root.saveConfig(); + SpinBoxRow { + label: qsTr("Spacing scale") + min: 0.1 + max: 5 + value: root.spacingScale + onValueModified: value => { + root.spacingScale = value; + root.saveConfig(); + } } } - } - CollapsibleSection { - id: transparencySection - title: qsTr("Transparency") - onToggleRequested: { - root.collapseAllSections(transparencySection); - } + CollapsibleSection { + id: transparencySection + title: qsTr("Transparency") + onToggleRequested: { + root.collapseAllSections(transparencySection); + } - SwitchRow { - label: qsTr("Transparency enabled") - checked: root.transparencyEnabled - onToggled: checked => { - root.transparencyEnabled = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Transparency enabled") + checked: root.transparencyEnabled + onToggled: checked => { + root.transparencyEnabled = checked; + root.saveConfig(); + } } - } - SpinBoxRow { - label: qsTr("Transparency base") - min: 0 - max: 1 - value: root.transparencyBase - onValueModified: value => { - root.transparencyBase = value; - root.saveConfig(); + SpinBoxRow { + label: qsTr("Transparency base") + min: 0 + max: 1 + value: root.transparencyBase + onValueModified: value => { + root.transparencyBase = value; + root.saveConfig(); + } } - } - SpinBoxRow { - label: qsTr("Transparency layers") - min: 0 - max: 1 - value: root.transparencyLayers - onValueModified: value => { - root.transparencyLayers = value; - root.saveConfig(); + SpinBoxRow { + label: qsTr("Transparency layers") + min: 0 + max: 1 + value: root.transparencyLayers + onValueModified: value => { + root.transparencyLayers = value; + root.saveConfig(); + } } } - } - CollapsibleSection { - id: borderSection - title: qsTr("Border") - onToggleRequested: { - root.collapseAllSections(borderSection); - } + CollapsibleSection { + id: borderSection + title: qsTr("Border") + onToggleRequested: { + root.collapseAllSections(borderSection); + } - SpinBoxRow { - label: qsTr("Border rounding") - min: 0.1 - max: 5 - value: root.borderRounding - onValueModified: value => { - root.borderRounding = value; - root.saveConfig(); + SpinBoxRow { + label: qsTr("Border rounding") + min: 0.1 + max: 5 + value: root.borderRounding + onValueModified: value => { + root.borderRounding = value; + root.saveConfig(); + } } - } - SpinBoxRow { - label: qsTr("Border thickness") - min: 0.1 - max: 5 - value: root.borderThickness - onValueModified: value => { - root.borderThickness = value; - root.saveConfig(); + SpinBoxRow { + label: qsTr("Border thickness") + min: 0.1 + max: 5 + value: root.borderThickness + onValueModified: value => { + root.borderThickness = value; + root.saveConfig(); + } } } - } - CollapsibleSection { - id: backgroundSection - title: qsTr("Background") - onToggleRequested: { - root.collapseAllSections(backgroundSection); - } + CollapsibleSection { + id: backgroundSection + title: qsTr("Background") + onToggleRequested: { + root.collapseAllSections(backgroundSection); + } - SwitchRow { - label: qsTr("Desktop clock") - checked: root.desktopClockEnabled - onToggled: checked => { - root.desktopClockEnabled = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Desktop clock") + checked: root.desktopClockEnabled + onToggled: checked => { + root.desktopClockEnabled = checked; + root.saveConfig(); + } } - } - SwitchRow { - label: qsTr("Background enabled") - checked: root.backgroundEnabled - onToggled: checked => { - root.backgroundEnabled = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Background enabled") + checked: root.backgroundEnabled + onToggled: checked => { + root.backgroundEnabled = checked; + root.saveConfig(); + } } - } - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Visualiser") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Visualiser") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } - SwitchRow { - label: qsTr("Visualiser enabled") - checked: root.visualiserEnabled - onToggled: checked => { - root.visualiserEnabled = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Visualiser enabled") + checked: root.visualiserEnabled + onToggled: checked => { + root.visualiserEnabled = checked; + root.saveConfig(); + } } - } - SwitchRow { - label: qsTr("Visualiser auto hide") - checked: root.visualiserAutoHide - onToggled: checked => { - root.visualiserAutoHide = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Visualiser auto hide") + checked: root.visualiserAutoHide + onToggled: checked => { + root.visualiserAutoHide = checked; + root.saveConfig(); + } } - } - SpinBoxRow { - label: qsTr("Visualiser rounding") - min: 0 - max: 10 - value: Math.round(root.visualiserRounding) - onValueModified: value => { - root.visualiserRounding = value; - root.saveConfig(); + SpinBoxRow { + label: qsTr("Visualiser rounding") + min: 0 + max: 10 + value: Math.round(root.visualiserRounding) + onValueModified: value => { + root.visualiserRounding = value; + root.saveConfig(); + } } - } - SpinBoxRow { - label: qsTr("Visualiser spacing") - min: 0 - max: 10 - value: Math.round(root.visualiserSpacing) - onValueModified: value => { - root.visualiserSpacing = value; - root.saveConfig(); + SpinBoxRow { + label: qsTr("Visualiser spacing") + min: 0 + max: 10 + value: Math.round(root.visualiserSpacing) + onValueModified: value => { + root.visualiserSpacing = value; + root.saveConfig(); + } } } } - } } InnerBorder { @@ -862,34 +871,34 @@ RowLayout { spacing: Appearance.spacing.normal - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "palette" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + 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.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.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 - } + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Select a wallpaper") + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } GridLayout { Layout.fillWidth: true @@ -948,10 +957,7 @@ RowLayout { anchors.fill: parent // Ensure sourceSize is always set to valid dimensions - sourceSize: Qt.size( - Math.max(1, Math.floor(parent.width)), - Math.max(1, Math.floor(parent.height)) - ) + sourceSize: Qt.size(Math.max(1, Math.floor(parent.width)), Math.max(1, Math.floor(parent.height))) // Show when ready, hide if fallback is showing opacity: status === Image.Ready && !fallbackImage.visible ? 1 : 0 @@ -971,10 +977,7 @@ RowLayout { source: modelData.path asynchronous: true fillMode: Image.PreserveAspectCrop - sourceSize: Qt.size( - Math.max(1, Math.floor(parent.width)), - Math.max(1, Math.floor(parent.height)) - ) + sourceSize: Qt.size(Math.max(1, Math.floor(parent.width)), Math.max(1, Math.floor(parent.height))) // Show if caching image hasn't loaded after a delay visible: opacity > 0 @@ -1019,24 +1022,35 @@ RowLayout { color: Colours.palette.m3primary font.pointSize: Appearance.font.size.large } - } - // Gradient overlay for filename with rounded bottom corners - Rectangle { - id: filenameOverlay - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: filenameText.implicitHeight + Appearance.padding.normal * 2 + // Gradient overlay for filename with rounded bottom corners + Rectangle { + id: filenameOverlay - // Match the parent's rounded corners at the bottom - radius: Appearance.rounding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 2 - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0) } - GradientStop { position: 0.3; color: Qt.rgba(0, 0, 0, 0.3) } - GradientStop { position: 0.7; color: Qt.rgba(0, 0, 0, 0.75) } - GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.85) } + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.rgba(0, 0, 0, 0) + } + GradientStop { + position: 0.3 + color: Qt.rgba(0, 0, 0, 0.3) + } + GradientStop { + position: 0.7 + color: Qt.rgba(0, 0, 0, 0.75) + } + GradientStop { + position: 1.0 + color: Qt.rgba(0, 0, 0, 0.85) + } + } } opacity: 0 @@ -1075,10 +1089,6 @@ RowLayout { elide: Text.ElideMiddle maximumLineCount: 1 - // Text shadow for better readability - style: Text.Outline - styleColor: Qt.rgba(0, 0, 0, 0.6) - opacity: 0 Behavior on opacity { @@ -1103,5 +1113,3 @@ RowLayout { } } } - - -- cgit v1.2.3-freya From f79fd9328ce01bde921775a0301a3a6969deaa34 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 10:25:45 -0500 Subject: controlcenter: fix edge to edge in panels --- .../controlcenter/appearance/AppearancePane.qml | 12 ++++++----- modules/controlcenter/audio/AudioPane.qml | 18 ++++++++++------ modules/controlcenter/bluetooth/BtPane.qml | 3 --- modules/controlcenter/launcher/LauncherPane.qml | 14 +++++++++--- modules/controlcenter/network/NetworkingPane.qml | 25 +++++++++------------- modules/controlcenter/taskbar/TaskbarPane.qml | 12 ++++++----- 6 files changed, 46 insertions(+), 38 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 1e81205..35b479a 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -114,8 +114,7 @@ RowLayout { id: sidebarFlickable anchors.fill: parent flickableDirection: Flickable.VerticalFlick - contentHeight: sidebarLayout.implicitHeight + Appearance.padding.large * 2 - clip: true + contentHeight: sidebarLayout.height StyledScrollBar.vertical: StyledScrollBar { flickable: sidebarFlickable @@ -852,11 +851,12 @@ RowLayout { StyledFlickable { anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.implicitHeight - clip: true + contentHeight: contentLayout.height StyledScrollBar.vertical: StyledScrollBar { flickable: parent @@ -868,6 +868,8 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top + anchors.leftMargin: Appearance.padding.large * 2 + anchors.rightMargin: Appearance.padding.large * 2 spacing: Appearance.spacing.normal diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index b4a0bf1..1f8677e 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -31,7 +31,10 @@ RowLayout { anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 flickableDirection: Flickable.VerticalFlick contentHeight: leftContent.height - clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } ColumnLayout { id: leftContent @@ -225,17 +228,16 @@ RowLayout { Layout.fillHeight: true StyledFlickable { - id: rightFlickable - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.implicitHeight - clip: true + contentHeight: contentLayout.height StyledScrollBar.vertical: StyledScrollBar { - flickable: rightFlickable + flickable: parent } ColumnLayout { @@ -244,6 +246,8 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top + anchors.leftMargin: Appearance.padding.large * 2 + anchors.rightMargin: Appearance.padding.large * 2 spacing: Appearance.spacing.normal diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 40cf356..96dc002 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -50,7 +50,6 @@ RowLayout { radius: rightBorder.innerRadius color: "transparent" - clip: true Loader { id: loader @@ -60,7 +59,6 @@ RowLayout { anchors.fill: parent anchors.margins: Appearance.padding.large * 2 - clip: true asynchronous: true sourceComponent: pane ? details : settings @@ -108,7 +106,6 @@ RowLayout { StyledFlickable { flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height - clip: true Settings { id: settingsInner diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index d585c32..1869e18 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -153,6 +153,7 @@ RowLayout { anchors.margins: Appearance.padding.large + Appearance.padding.normal anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 + anchors.bottomMargin: 0 spacing: Appearance.spacing.small @@ -335,12 +336,16 @@ RowLayout { ColumnLayout { anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 spacing: Appearance.spacing.normal Item { Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: Appearance.padding.large * 2 + Layout.rightMargin: Appearance.padding.large * 2 implicitWidth: iconLoader.implicitWidth implicitHeight: iconLoader.implicitHeight @@ -376,6 +381,8 @@ RowLayout { StyledText { Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: Appearance.padding.large * 2 + Layout.rightMargin: Appearance.padding.large * 2 text: root.selectedApp ? (root.selectedApp.name || root.selectedApp.entry?.name || qsTr("Application Details")) : qsTr("Launcher Applications") font.pointSize: Appearance.font.size.large font.bold: true @@ -385,12 +392,13 @@ RowLayout { Layout.fillWidth: true Layout.fillHeight: true Layout.topMargin: Appearance.spacing.large + Layout.leftMargin: Appearance.padding.large * 2 + Layout.rightMargin: Appearance.padding.large * 2 StyledFlickable { anchors.fill: parent flickableDirection: Flickable.VerticalFlick - contentHeight: debugLayout.implicitHeight - clip: true + contentHeight: debugLayout.height StyledScrollBar.vertical: StyledScrollBar { flickable: parent diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 74e0034..c404af0 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -38,12 +38,8 @@ Item { id: leftFlickable 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 flickableDirection: Flickable.VerticalFlick contentHeight: leftContent.height - clip: true StyledScrollBar.vertical: StyledScrollBar { flickable: leftFlickable @@ -54,6 +50,10 @@ Item { anchors.left: parent.left anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 spacing: Appearance.spacing.normal // Settings header above the collapsible sections @@ -404,7 +404,6 @@ Item { radius: rightBorder.innerRadius color: "transparent" - clip: true // Right pane - networking details/settings Loader { @@ -415,14 +414,17 @@ Item { property var pane: ethernetPane || wirelessPane property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "") - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.leftMargin: Appearance.padding.large * 2 + anchors.rightMargin: Appearance.padding.large * 2 opacity: 1 scale: 1 transformOrigin: Item.Center - clip: true asynchronous: true sourceComponent: pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings @@ -476,15 +478,8 @@ Item { id: settings StyledFlickable { - id: settingsFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } NetworkSettings { id: settingsInner diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 5385ab7..9ec9e2a 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -134,8 +134,7 @@ RowLayout { id: sidebarFlickable anchors.fill: parent flickableDirection: Flickable.VerticalFlick - contentHeight: sidebarLayout.implicitHeight + Appearance.padding.large * 2 - clip: true + contentHeight: sidebarLayout.height StyledScrollBar.vertical: StyledScrollBar { flickable: sidebarFlickable @@ -531,11 +530,12 @@ RowLayout { StyledFlickable { anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.implicitHeight - clip: true + contentHeight: contentLayout.height StyledScrollBar.vertical: StyledScrollBar { flickable: parent @@ -547,6 +547,8 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top + anchors.leftMargin: Appearance.padding.large * 2 + anchors.rightMargin: Appearance.padding.large * 2 spacing: Appearance.spacing.normal -- cgit v1.2.3-freya From bfe0905d4760ad14eaeaf6beb0a75fc2ca77a332 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 10:58:04 -0500 Subject: controlcenter: correcting margins (networking details pane) --- modules/controlcenter/network/NetworkingPane.qml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index c404af0..ada29dc 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -414,12 +414,8 @@ Item { property var pane: ethernetPane || wirelessPane property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "") - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.leftMargin: Appearance.padding.large * 2 - anchors.rightMargin: Appearance.padding.large * 2 + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 opacity: 1 scale: 1 -- cgit v1.2.3-freya From f3a0fcc715a8b2fd2a588b5fa13301033380a2ed Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 11:08:03 -0500 Subject: controlcenter: correcting margin inconsistencies between panels --- modules/controlcenter/appearance/AppearancePane.qml | 1 + modules/controlcenter/audio/AudioPane.qml | 1 + modules/controlcenter/launcher/LauncherPane.qml | 1 + modules/controlcenter/taskbar/TaskbarPane.qml | 1 + 4 files changed, 4 insertions(+) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 35b479a..c9d62a0 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -870,6 +870,7 @@ RowLayout { anchors.top: parent.top anchors.leftMargin: Appearance.padding.large * 2 anchors.rightMargin: Appearance.padding.large * 2 + anchors.topMargin: Appearance.padding.large * 2 spacing: Appearance.spacing.normal diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 1f8677e..add7078 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -248,6 +248,7 @@ RowLayout { anchors.top: parent.top anchors.leftMargin: Appearance.padding.large * 2 anchors.rightMargin: Appearance.padding.large * 2 + anchors.topMargin: Appearance.padding.large * 2 spacing: Appearance.spacing.normal diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 1869e18..8ddccb4 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -346,6 +346,7 @@ RowLayout { 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 diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 9ec9e2a..107091e 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -549,6 +549,7 @@ RowLayout { anchors.top: parent.top anchors.leftMargin: Appearance.padding.large * 2 anchors.rightMargin: Appearance.padding.large * 2 + anchors.topMargin: Appearance.padding.large * 2 spacing: Appearance.spacing.normal -- cgit v1.2.3-freya From 546eaa7d4260e9c0f918cfb2a4853d289d807e94 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 11:13:11 -0500 Subject: controlcenter: appearance transparency values now use sliders --- .../controlcenter/appearance/AppearancePane.qml | 98 ++++++++++++++++++---- 1 file changed, 82 insertions(+), 16 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index c9d62a0..d78ab21 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -711,25 +711,91 @@ RowLayout { } } - SpinBoxRow { - label: qsTr("Transparency base") - min: 0 - max: 1 - value: root.transparencyBase - onValueModified: value => { - root.transparencyBase = value; - root.saveConfig(); + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Transparency base") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: qsTr("%1%").arg(Math.round(root.transparencyBase * 100)) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: baseSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0 + to: 100 + value: root.transparencyBase * 100 + onMoved: { + root.transparencyBase = baseSlider.value / 100; + root.saveConfig(); + } + } } } - SpinBoxRow { - label: qsTr("Transparency layers") - min: 0 - max: 1 - value: root.transparencyLayers - onValueModified: value => { - root.transparencyLayers = value; - root.saveConfig(); + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Transparency layers") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: qsTr("%1%").arg(Math.round(root.transparencyLayers * 100)) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: layersSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0 + to: 100 + value: root.transparencyLayers * 100 + onMoved: { + root.transparencyLayers = layersSlider.value / 100; + root.saveConfig(); + } + } } } } -- cgit v1.2.3-freya From 442b49e0e1975d51efcef90d10566c90dedbf7e1 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 11:20:06 -0500 Subject: controlcenter: appearance pane corrections to fp/int values such as scales and border --- components/controls/CustomSpinBox.qml | 74 ++++++++++++++++++++-- components/controls/SpinBoxRow.qml | 2 + .../controlcenter/appearance/AppearancePane.qml | 11 +++- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml index e2ed508..438dc08 100644 --- a/components/controls/CustomSpinBox.qml +++ b/components/controls/CustomSpinBox.qml @@ -9,19 +9,69 @@ import QtQuick.Layouts RowLayout { id: root - property int value + property real value property real max: Infinity property real min: -Infinity + property real step: 1 property alias repeatRate: timer.interval - signal valueModified(value: int) + signal valueModified(value: real) spacing: Appearance.spacing.small + property bool isEditing: false + property string displayText: root.value.toString() + + onValueChanged: { + if (!root.isEditing) { + root.displayText = root.value.toString(); + } + } + StyledTextField { + id: textField + inputMethodHints: Qt.ImhFormattedNumbersOnly - text: root.value - onAccepted: root.valueModified(text) + text: root.isEditing ? text : root.displayText + validator: DoubleValidator { + bottom: root.min + top: root.max + decimals: root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0 + } + onActiveFocusChanged: { + if (activeFocus) { + root.isEditing = true; + } else { + root.isEditing = false; + root.displayText = root.value.toString(); + } + } + onAccepted: { + const numValue = parseFloat(text); + if (!isNaN(numValue)) { + const clampedValue = Math.max(root.min, Math.min(root.max, numValue)); + root.value = clampedValue; + root.displayText = clampedValue.toString(); + root.valueModified(clampedValue); + } else { + text = root.displayText; + } + root.isEditing = false; + } + onEditingFinished: { + if (text !== root.displayText) { + const numValue = parseFloat(text); + if (!isNaN(numValue)) { + const clampedValue = Math.max(root.min, Math.min(root.max, numValue)); + root.value = clampedValue; + root.displayText = clampedValue.toString(); + root.valueModified(clampedValue); + } else { + text = root.displayText; + } + } + root.isEditing = false; + } padding: Appearance.padding.small leftPadding: Appearance.padding.normal @@ -50,7 +100,13 @@ RowLayout { onReleased: timer.stop() function onClicked(): void { - root.valueModified(Math.min(root.max, root.value + 1)); + let newValue = Math.min(root.max, root.value + root.step); + // Round to avoid floating point precision errors + const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; + newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals); + root.value = newValue; + root.displayText = newValue.toString(); + root.valueModified(newValue); } } @@ -79,7 +135,13 @@ RowLayout { onReleased: timer.stop() function onClicked(): void { - root.valueModified(Math.max(root.min, root.value - 1)); + let newValue = Math.max(root.min, root.value - root.step); + // Round to avoid floating point precision errors + const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; + newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals); + root.value = newValue; + root.displayText = newValue.toString(); + root.valueModified(newValue); } } diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml index a4441c5..2507b80 100644 --- a/components/controls/SpinBoxRow.qml +++ b/components/controls/SpinBoxRow.qml @@ -13,6 +13,7 @@ StyledRect { required property real value required property real min required property real max + property real step: 1 property var onValueModified: function(value) {} Layout.fillWidth: true @@ -41,6 +42,7 @@ StyledRect { CustomSpinBox { min: root.min max: root.max + step: root.step value: root.value onValueModified: value => { root.onValueModified(value); diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index d78ab21..6781cf0 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -408,6 +408,7 @@ RowLayout { label: qsTr("Animation duration scale") min: 0.1 max: 5 + step: 0.1 value: root.animDurationsScale onValueModified: value => { root.animDurationsScale = value; @@ -646,6 +647,7 @@ RowLayout { label: qsTr("Font size scale") min: 0.1 max: 5 + step: 0.1 value: root.fontSizeScale onValueModified: value => { root.fontSizeScale = value; @@ -665,6 +667,7 @@ RowLayout { label: qsTr("Padding scale") min: 0.1 max: 5 + step: 0.1 value: root.paddingScale onValueModified: value => { root.paddingScale = value; @@ -676,6 +679,7 @@ RowLayout { label: qsTr("Rounding scale") min: 0.1 max: 5 + step: 0.1 value: root.roundingScale onValueModified: value => { root.roundingScale = value; @@ -687,6 +691,7 @@ RowLayout { label: qsTr("Spacing scale") min: 0.1 max: 5 + step: 0.1 value: root.spacingScale onValueModified: value => { root.spacingScale = value; @@ -810,7 +815,8 @@ RowLayout { SpinBoxRow { label: qsTr("Border rounding") min: 0.1 - max: 5 + max: 100 + step: 0.1 value: root.borderRounding onValueModified: value => { root.borderRounding = value; @@ -821,7 +827,8 @@ RowLayout { SpinBoxRow { label: qsTr("Border thickness") min: 0.1 - max: 5 + max: 100 + step: 0.1 value: root.borderThickness onValueModified: value => { root.borderThickness = value; -- cgit v1.2.3-freya From b0006f2f1146c14f4a8d719d6a268ffce1fed0de Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 16:44:11 -0500 Subject: controlcenter: corrected all panels edge-to-edge containers --- .../controlcenter/appearance/AppearancePane.qml | 295 ++++++++++++--------- modules/controlcenter/audio/AudioPane.qml | 124 ++++++--- modules/controlcenter/bluetooth/BtPane.qml | 37 ++- modules/controlcenter/launcher/LauncherPane.qml | 87 ++++-- modules/controlcenter/network/NetworkingPane.qml | 63 +++-- modules/controlcenter/taskbar/TaskbarPane.qml | 155 ++++++----- 6 files changed, 491 insertions(+), 270 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 6781cf0..ba95977 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -12,6 +12,7 @@ import qs.config import qs.utils import Caelestia.Models import Quickshell +import Quickshell.Widgets import QtQuick import QtQuick.Layouts @@ -47,26 +48,6 @@ RowLayout { spacing: 0 - function collapseAllSections(exceptSection) { - if (exceptSection !== themeModeSection) - themeModeSection.expanded = false; - if (exceptSection !== colorVariantSection) - colorVariantSection.expanded = false; - if (exceptSection !== colorSchemeSection) - colorSchemeSection.expanded = false; - if (exceptSection !== animationsSection) - animationsSection.expanded = false; - if (exceptSection !== fontsSection) - fontsSection.expanded = false; - if (exceptSection !== scalesSection) - scalesSection.expanded = false; - if (exceptSection !== transparencySection) - transparencySection.expanded = false; - if (exceptSection !== borderSection) - borderSection.expanded = false; - if (exceptSection !== backgroundSection) - backgroundSection.expanded = false; - } function saveConfig() { // Update animations @@ -110,26 +91,72 @@ RowLayout { Layout.minimumWidth: 420 Layout.fillHeight: true - StyledFlickable { - id: sidebarFlickable + ClippingRectangle { + id: leftAppearanceClippingRect anchors.fill: parent - flickableDirection: Flickable.VerticalFlick - contentHeight: sidebarLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: sidebarFlickable - } + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + radius: leftAppearanceBorder.innerRadius + color: "transparent" - ColumnLayout { - id: sidebarLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top + 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 + } + } + + InnerBorder { + id: leftAppearanceBorder + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + + Component { + id: appearanceLeftContentComponent + + StyledFlickable { + id: sidebarFlickable + readonly property var rootPane: leftAppearanceLoader.rootPane + flickableDirection: Flickable.VerticalFlick + contentHeight: sidebarLayout.height + + function collapseAllSections(exceptSection) { + if (exceptSection !== themeModeSection) + themeModeSection.expanded = false; + if (exceptSection !== colorVariantSection) + colorVariantSection.expanded = false; + if (exceptSection !== colorSchemeSection) + colorSchemeSection.expanded = false; + if (exceptSection !== animationsSection) + animationsSection.expanded = false; + if (exceptSection !== fontsSection) + fontsSection.expanded = false; + if (exceptSection !== scalesSection) + scalesSection.expanded = false; + if (exceptSection !== transparencySection) + transparencySection.expanded = false; + if (exceptSection !== borderSection) + borderSection.expanded = false; + if (exceptSection !== backgroundSection) + backgroundSection.expanded = false; + } - spacing: Appearance.spacing.small + StyledScrollBar.vertical: StyledScrollBar { + flickable: sidebarFlickable + } + + ColumnLayout { + id: sidebarLayout + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small RowLayout { spacing: Appearance.spacing.smaller @@ -150,7 +177,7 @@ RowLayout { title: qsTr("Theme mode") description: qsTr("Light or dark theme") onToggleRequested: { - root.collapseAllSections(themeModeSection); + sidebarFlickable.collapseAllSections(themeModeSection); } SwitchRow { @@ -167,7 +194,7 @@ RowLayout { title: qsTr("Color variant") description: qsTr("Material theme variant") onToggleRequested: { - root.collapseAllSections(colorVariantSection); + sidebarFlickable.collapseAllSections(colorVariantSection); } StyledListView { @@ -257,7 +284,7 @@ RowLayout { title: qsTr("Color scheme") description: qsTr("Available color schemes") onToggleRequested: { - root.collapseAllSections(colorSchemeSection); + sidebarFlickable.collapseAllSections(colorSchemeSection); } StyledListView { @@ -401,7 +428,7 @@ RowLayout { id: animationsSection title: qsTr("Animations") onToggleRequested: { - root.collapseAllSections(animationsSection); + sidebarFlickable.collapseAllSections(animationsSection); } SpinBoxRow { @@ -409,10 +436,10 @@ RowLayout { min: 0.1 max: 5 step: 0.1 - value: root.animDurationsScale + value: rootPane.animDurationsScale onValueModified: value => { - root.animDurationsScale = value; - root.saveConfig(); + rootPane.animDurationsScale = value; + rootPane.saveConfig(); } } } @@ -421,7 +448,7 @@ RowLayout { id: fontsSection title: qsTr("Fonts") onToggleRequested: { - root.collapseAllSections(fontsSection); + sidebarFlickable.collapseAllSections(fontsSection); } StyledText { @@ -449,7 +476,7 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right - readonly property bool isCurrent: modelData === root.fontFamilyMaterial + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 @@ -457,8 +484,8 @@ RowLayout { StateLayer { function onClicked(): void { - root.fontFamilyMaterial = modelData; - root.saveConfig(); + rootPane.fontFamilyMaterial = modelData; + rootPane.saveConfig(); } } @@ -522,7 +549,7 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right - readonly property bool isCurrent: modelData === root.fontFamilyMono + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 @@ -530,8 +557,8 @@ RowLayout { StateLayer { function onClicked(): void { - root.fontFamilyMono = modelData; - root.saveConfig(); + rootPane.fontFamilyMono = modelData; + rootPane.saveConfig(); } } @@ -595,7 +622,7 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right - readonly property bool isCurrent: modelData === root.fontFamilySans + readonly property bool isCurrent: modelData === rootPane.fontFamilySans color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 @@ -603,8 +630,8 @@ RowLayout { StateLayer { function onClicked(): void { - root.fontFamilySans = modelData; - root.saveConfig(); + rootPane.fontFamilySans = modelData; + rootPane.saveConfig(); } } @@ -648,10 +675,10 @@ RowLayout { min: 0.1 max: 5 step: 0.1 - value: root.fontSizeScale + value: rootPane.fontSizeScale onValueModified: value => { - root.fontSizeScale = value; - root.saveConfig(); + rootPane.fontSizeScale = value; + rootPane.saveConfig(); } } } @@ -660,7 +687,7 @@ RowLayout { id: scalesSection title: qsTr("Scales") onToggleRequested: { - root.collapseAllSections(scalesSection); + sidebarFlickable.collapseAllSections(scalesSection); } SpinBoxRow { @@ -668,10 +695,10 @@ RowLayout { min: 0.1 max: 5 step: 0.1 - value: root.paddingScale + value: rootPane.paddingScale onValueModified: value => { - root.paddingScale = value; - root.saveConfig(); + rootPane.paddingScale = value; + rootPane.saveConfig(); } } @@ -680,10 +707,10 @@ RowLayout { min: 0.1 max: 5 step: 0.1 - value: root.roundingScale + value: rootPane.roundingScale onValueModified: value => { - root.roundingScale = value; - root.saveConfig(); + rootPane.roundingScale = value; + rootPane.saveConfig(); } } @@ -692,10 +719,10 @@ RowLayout { min: 0.1 max: 5 step: 0.1 - value: root.spacingScale + value: rootPane.spacingScale onValueModified: value => { - root.spacingScale = value; - root.saveConfig(); + rootPane.spacingScale = value; + rootPane.saveConfig(); } } } @@ -704,15 +731,15 @@ RowLayout { id: transparencySection title: qsTr("Transparency") onToggleRequested: { - root.collapseAllSections(transparencySection); + sidebarFlickable.collapseAllSections(transparencySection); } SwitchRow { label: qsTr("Transparency enabled") - checked: root.transparencyEnabled + checked: rootPane.transparencyEnabled onToggled: checked => { - root.transparencyEnabled = checked; - root.saveConfig(); + rootPane.transparencyEnabled = checked; + rootPane.saveConfig(); } } @@ -737,7 +764,7 @@ RowLayout { } StyledText { - text: qsTr("%1%").arg(Math.round(root.transparencyBase * 100)) + text: qsTr("%1%").arg(Math.round(rootPane.transparencyBase * 100)) color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal } @@ -751,10 +778,10 @@ RowLayout { from: 0 to: 100 - value: root.transparencyBase * 100 + value: rootPane.transparencyBase * 100 onMoved: { - root.transparencyBase = baseSlider.value / 100; - root.saveConfig(); + rootPane.transparencyBase = baseSlider.value / 100; + rootPane.saveConfig(); } } } @@ -781,7 +808,7 @@ RowLayout { } StyledText { - text: qsTr("%1%").arg(Math.round(root.transparencyLayers * 100)) + text: qsTr("%1%").arg(Math.round(rootPane.transparencyLayers * 100)) color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal } @@ -795,10 +822,10 @@ RowLayout { from: 0 to: 100 - value: root.transparencyLayers * 100 + value: rootPane.transparencyLayers * 100 onMoved: { - root.transparencyLayers = layersSlider.value / 100; - root.saveConfig(); + rootPane.transparencyLayers = layersSlider.value / 100; + rootPane.saveConfig(); } } } @@ -809,7 +836,7 @@ RowLayout { id: borderSection title: qsTr("Border") onToggleRequested: { - root.collapseAllSections(borderSection); + sidebarFlickable.collapseAllSections(borderSection); } SpinBoxRow { @@ -817,10 +844,10 @@ RowLayout { min: 0.1 max: 100 step: 0.1 - value: root.borderRounding + value: rootPane.borderRounding onValueModified: value => { - root.borderRounding = value; - root.saveConfig(); + rootPane.borderRounding = value; + rootPane.saveConfig(); } } @@ -829,10 +856,10 @@ RowLayout { min: 0.1 max: 100 step: 0.1 - value: root.borderThickness + value: rootPane.borderThickness onValueModified: value => { - root.borderThickness = value; - root.saveConfig(); + rootPane.borderThickness = value; + rootPane.saveConfig(); } } } @@ -841,24 +868,24 @@ RowLayout { id: backgroundSection title: qsTr("Background") onToggleRequested: { - root.collapseAllSections(backgroundSection); + sidebarFlickable.collapseAllSections(backgroundSection); } SwitchRow { label: qsTr("Desktop clock") - checked: root.desktopClockEnabled + checked: rootPane.desktopClockEnabled onToggled: checked => { - root.desktopClockEnabled = checked; - root.saveConfig(); + rootPane.desktopClockEnabled = checked; + rootPane.saveConfig(); } } SwitchRow { label: qsTr("Background enabled") - checked: root.backgroundEnabled + checked: rootPane.backgroundEnabled onToggled: checked => { - root.backgroundEnabled = checked; - root.saveConfig(); + rootPane.backgroundEnabled = checked; + rootPane.saveConfig(); } } @@ -871,19 +898,19 @@ RowLayout { SwitchRow { label: qsTr("Visualiser enabled") - checked: root.visualiserEnabled + checked: rootPane.visualiserEnabled onToggled: checked => { - root.visualiserEnabled = checked; - root.saveConfig(); + rootPane.visualiserEnabled = checked; + rootPane.saveConfig(); } } SwitchRow { label: qsTr("Visualiser auto hide") - checked: root.visualiserAutoHide + checked: rootPane.visualiserAutoHide onToggled: checked => { - root.visualiserAutoHide = checked; - root.saveConfig(); + rootPane.visualiserAutoHide = checked; + rootPane.saveConfig(); } } @@ -891,10 +918,10 @@ RowLayout { label: qsTr("Visualiser rounding") min: 0 max: 10 - value: Math.round(root.visualiserRounding) + value: Math.round(rootPane.visualiserRounding) onValueModified: value => { - root.visualiserRounding = value; - root.saveConfig(); + rootPane.visualiserRounding = value; + rootPane.saveConfig(); } } @@ -902,19 +929,15 @@ RowLayout { label: qsTr("Visualiser spacing") min: 0 max: 10 - value: Math.round(root.visualiserSpacing) + value: Math.round(rootPane.visualiserSpacing) onValueModified: value => { - root.visualiserSpacing = value; - root.saveConfig(); + rootPane.visualiserSpacing = value; + rootPane.saveConfig(); } } } } - } - - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 + } } } @@ -922,30 +945,53 @@ RowLayout { Layout.fillWidth: true Layout.fillHeight: true - StyledFlickable { + 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" - flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent + Loader { + id: rightAppearanceLoader + anchors.fill: parent + anchors.topMargin: Appearance.padding.large * 2 + anchors.bottomMargin: Appearance.padding.large * 2 + anchors.leftMargin: 0 + anchors.rightMargin: 0 + asynchronous: true + sourceComponent: appearanceRightContentComponent + property var rootPane: root } + } - ColumnLayout { - id: contentLayout + InnerBorder { + id: rightAppearanceBorder + leftThickness: Appearance.padding.normal / 2 + } - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.leftMargin: Appearance.padding.large * 2 - anchors.rightMargin: Appearance.padding.large * 2 - anchors.topMargin: Appearance.padding.large * 2 + Component { + id: appearanceRightContentComponent - spacing: Appearance.spacing.normal + 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.leftMargin: Appearance.padding.large * 2 + anchors.rightMargin: Appearance.padding.large * 2 + spacing: Appearance.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter @@ -1182,10 +1228,7 @@ RowLayout { } } } - } - - InnerBorder { - leftThickness: Appearance.padding.normal / 2 + } } } } diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index add7078..005de3a 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -7,6 +7,7 @@ import qs.components.effects import qs.components.containers import qs.services import qs.config +import Quickshell.Widgets import QtQuick import QtQuick.Layouts @@ -20,28 +21,58 @@ RowLayout { spacing: 0 Item { + id: leftAudioItem Layout.preferredWidth: Math.floor(parent.width * 0.4) Layout.minimumWidth: 420 Layout.fillHeight: true - StyledFlickable { + ClippingRectangle { + id: leftAudioClippingRect 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 - flickableDirection: Flickable.VerticalFlick - contentHeight: leftContent.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: 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 { + id: leftAudioFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContent.height - ColumnLayout { - id: leftContent + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftAudioFlickable + } + + ColumnLayout { + id: leftContent - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.normal + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal // Settings header above the collapsible sections RowLayout { @@ -215,42 +246,64 @@ RowLayout { } } } - } - - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 + } } } Item { + id: rightAudioItem Layout.fillWidth: true Layout.fillHeight: true - StyledFlickable { + ClippingRectangle { + id: rightAudioClippingRect anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 anchors.rightMargin: Appearance.padding.normal / 2 - flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.height + radius: rightAudioBorder.innerRadius + color: "transparent" + + Loader { + id: rightAudioLoader + + anchors.fill: parent + anchors.topMargin: Appearance.padding.large * 2 + anchors.bottomMargin: Appearance.padding.large * 2 + anchors.leftMargin: 0 + anchors.rightMargin: 0 - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent + asynchronous: true + sourceComponent: audioRightContentComponent } + } + + InnerBorder { + id: rightAudioBorder + leftThickness: Appearance.padding.normal / 2 + } - ColumnLayout { - id: contentLayout + Component { + id: audioRightContentComponent - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.leftMargin: Appearance.padding.large * 2 - anchors.rightMargin: Appearance.padding.large * 2 - anchors.topMargin: Appearance.padding.large * 2 + StyledFlickable { + id: rightAudioFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: rightAudioFlickable + } + + ColumnLayout { + id: contentLayout - 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" @@ -397,10 +450,7 @@ RowLayout { } } } - } - - InnerBorder { - leftThickness: Appearance.padding.normal / 2 + } } } } \ No newline at end of file diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 96dc002..32d2c0d 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -19,30 +19,57 @@ RowLayout { spacing: 0 Item { + id: leftBtItem Layout.preferredWidth: Math.floor(parent.width * 0.4) Layout.minimumWidth: 420 Layout.fillHeight: true - DeviceList { + ClippingRectangle { + id: leftBtClippingRect 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 + 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 - session: root.session + asynchronous: true + sourceComponent: btDeviceListComponent + } } InnerBorder { + id: leftBtBorder leftThickness: 0 rightThickness: Appearance.padding.normal / 2 } + + Component { + id: btDeviceListComponent + + 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 diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 8ddccb4..12abc1e 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -144,20 +144,50 @@ RowLayout { } Item { + id: leftLauncherItem Layout.preferredWidth: Math.floor(parent.width * 0.4) Layout.minimumWidth: 420 Layout.fillHeight: true - ColumnLayout { + ClippingRectangle { + id: leftLauncherClippingRect 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 - anchors.bottomMargin: 0 + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + + radius: leftLauncherBorder.innerRadius + color: "transparent" + + Loader { + id: leftLauncherLoader - spacing: Appearance.spacing.small + 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 + anchors.bottomMargin: 0 - RowLayout { + asynchronous: true + sourceComponent: leftContentComponent + } + } + + InnerBorder { + id: leftLauncherBorder + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + + Component { + id: leftContentComponent + + ColumnLayout { + anchors.fill: parent + + spacing: Appearance.spacing.small + + RowLayout { spacing: Appearance.spacing.smaller StyledText { @@ -268,6 +298,7 @@ RowLayout { } StyledListView { + id: appsListView Layout.fillWidth: true Layout.fillHeight: true @@ -323,26 +354,50 @@ RowLayout { } } } - - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 } } Item { + id: rightLauncherItem Layout.fillWidth: true Layout.fillHeight: true - ColumnLayout { + ClippingRectangle { + id: rightLauncherClippingRect anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 anchors.rightMargin: Appearance.padding.normal / 2 - spacing: Appearance.spacing.normal + radius: rightLauncherBorder.innerRadius + color: "transparent" - Item { + Loader { + id: rightLauncherLoader + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + asynchronous: true + sourceComponent: rightContentComponent + } + } + + InnerBorder { + id: rightLauncherBorder + + leftThickness: Appearance.padding.normal / 2 + } + + Component { + id: rightContentComponent + + ColumnLayout { + anchors.fill: parent + + spacing: Appearance.spacing.normal + + Item { Layout.alignment: Qt.AlignHCenter Layout.leftMargin: Appearance.padding.large * 2 Layout.rightMargin: Appearance.padding.large * 2 @@ -397,6 +452,7 @@ RowLayout { Layout.rightMargin: Appearance.padding.large * 2 StyledFlickable { + id: detailsFlickable anchors.fill: parent flickableDirection: Flickable.VerticalFlick contentHeight: debugLayout.height @@ -428,9 +484,6 @@ RowLayout { } } } - - InnerBorder { - leftThickness: Appearance.padding.normal / 2 } } } diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index ada29dc..c3621bb 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -29,32 +29,59 @@ Item { spacing: 0 Item { + id: leftNetworkItem Layout.preferredWidth: Math.floor(parent.width * 0.4) Layout.minimumWidth: 420 Layout.fillHeight: true - // Left pane - networking list with collapsible sections - StyledFlickable { - id: leftFlickable - + ClippingRectangle { + id: leftNetworkClippingRect anchors.fill: parent - flickableDirection: Flickable.VerticalFlick - contentHeight: leftContent.height + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 - StyledScrollBar.vertical: StyledScrollBar { - flickable: leftFlickable - } + radius: leftNetworkBorder.innerRadius + color: "transparent" - ColumnLayout { - id: leftContent + Loader { + id: leftNetworkLoader - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top + 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 - spacing: Appearance.spacing.normal + + asynchronous: true + sourceComponent: networkListComponent + } + } + + InnerBorder { + id: leftNetworkBorder + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + + Component { + id: networkListComponent + + StyledFlickable { + id: leftFlickable + + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContent.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } + + ColumnLayout { + id: leftContent + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal // Settings header above the collapsible sections RowLayout { @@ -385,18 +412,16 @@ Item { } } } - - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 } } Item { + id: rightNetworkItem Layout.fillWidth: true Layout.fillHeight: true ClippingRectangle { + id: networkClippingRect anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 107091e..85e5275 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -9,6 +9,7 @@ import qs.services import qs.config import qs.utils import Quickshell +import Quickshell.Widgets import QtQuick import QtQuick.Layouts @@ -17,6 +18,9 @@ RowLayout { required property Session session + // Clock + property bool clockShowIcon: Config.bar.clock.showIcon ?? true + // Bar Behavior property bool persistent: Config.bar.persistent ?? true property bool showOnHover: Config.bar.showOnHover ?? true @@ -48,9 +52,6 @@ RowLayout { spacing: 0 Component.onCompleted: { - // Update clock toggle - clockShowIconSwitch.checked = Config.bar.clock.showIcon ?? true; - // Update entries if (Config.bar.entries) { entriesModel.clear(); @@ -66,7 +67,7 @@ RowLayout { function saveConfig(entryIndex, entryEnabled) { // Update clock setting - Config.bar.clock.showIcon = clockShowIconSwitch.checked; + Config.bar.clock.showIcon = root.clockShowIcon; // Update bar behavior Config.bar.persistent = root.persistent; @@ -116,42 +117,62 @@ RowLayout { id: entriesModel } - - function collapseAllSections(exceptSection) { - if (exceptSection !== clockSection) clockSection.expanded = false; - if (exceptSection !== barBehaviorSection) barBehaviorSection.expanded = false; - if (exceptSection !== statusIconsSection) statusIconsSection.expanded = false; - if (exceptSection !== traySettingsSection) traySettingsSection.expanded = false; - if (exceptSection !== workspacesSection) workspacesSection.expanded = false; - } - Item { + id: leftTaskbarItem Layout.preferredWidth: Math.floor(parent.width * 0.4) Layout.minimumWidth: 420 Layout.fillHeight: true - StyledFlickable { - id: sidebarFlickable + ClippingRectangle { + id: leftTaskbarClippingRect anchors.fill: parent - flickableDirection: Flickable.VerticalFlick - contentHeight: sidebarLayout.height + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 - StyledScrollBar.vertical: StyledScrollBar { - flickable: sidebarFlickable - } + radius: leftTaskbarBorder.innerRadius + color: "transparent" + + Loader { + id: leftTaskbarLoader - ColumnLayout { - id: sidebarLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top + 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 - spacing: Appearance.spacing.small + asynchronous: true + sourceComponent: leftTaskbarContentComponent + } + } - RowLayout { + InnerBorder { + id: leftTaskbarBorder + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + + Component { + id: leftTaskbarContentComponent + + StyledFlickable { + id: sidebarFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: sidebarLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: sidebarFlickable + } + + ColumnLayout { + id: sidebarLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + + spacing: Appearance.spacing.small + + RowLayout { spacing: Appearance.spacing.smaller StyledText { @@ -169,9 +190,6 @@ RowLayout { id: clockSection title: qsTr("Clock") description: qsTr("Clock display settings") - onToggleRequested: { - root.collapseAllSections(clockSection); - } RowLayout { id: clockRow @@ -190,8 +208,9 @@ RowLayout { StyledSwitch { id: clockShowIconSwitch - checked: true + checked: root.clockShowIcon onToggled: { + root.clockShowIcon = checked; root.saveConfig(); } } @@ -201,9 +220,6 @@ RowLayout { CollapsibleSection { id: barBehaviorSection title: qsTr("Bar Behavior") - onToggleRequested: { - root.collapseAllSections(barBehaviorSection); - } SwitchRow { label: qsTr("Persistent") @@ -238,9 +254,6 @@ RowLayout { CollapsibleSection { id: statusIconsSection title: qsTr("Status Icons") - onToggleRequested: { - root.collapseAllSections(statusIconsSection); - } SwitchRow { label: qsTr("Show audio") @@ -309,9 +322,6 @@ RowLayout { CollapsibleSection { id: traySettingsSection title: qsTr("Tray Settings") - onToggleRequested: { - root.collapseAllSections(traySettingsSection); - } SwitchRow { label: qsTr("Background") @@ -344,9 +354,6 @@ RowLayout { CollapsibleSection { id: workspacesSection title: qsTr("Workspaces") - onToggleRequested: { - root.collapseAllSections(workspacesSection); - } StyledRect { Layout.fillWidth: true @@ -517,43 +524,62 @@ RowLayout { } } } - - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 } } Item { + id: rightTaskbarItem Layout.fillWidth: true Layout.fillHeight: true - StyledFlickable { + ClippingRectangle { + id: rightTaskbarClippingRect anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 anchors.rightMargin: Appearance.padding.normal / 2 - flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.height + radius: rightTaskbarBorder.innerRadius + color: "transparent" + + Loader { + id: rightTaskbarLoader - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + asynchronous: true + sourceComponent: rightTaskbarContentComponent } + } + + InnerBorder { + id: rightTaskbarBorder + + leftThickness: Appearance.padding.normal / 2 + } + + Component { + id: rightTaskbarContentComponent + + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height - ColumnLayout { - id: contentLayout + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + ColumnLayout { + id: contentLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.leftMargin: Appearance.padding.large * 2 - anchors.rightMargin: Appearance.padding.large * 2 - anchors.topMargin: Appearance.padding.large * 2 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - MaterialIcon { + MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "task_alt" font.pointSize: Appearance.font.size.extraLarge * 3 @@ -577,7 +603,7 @@ RowLayout { StyledText { Layout.alignment: Qt.AlignHCenter - text: clockShowIconSwitch.checked ? qsTr("Clock icon enabled") : qsTr("Clock icon disabled") + text: root.clockShowIcon ? qsTr("Clock icon enabled") : qsTr("Clock icon disabled") color: Colours.palette.m3outline } @@ -597,9 +623,6 @@ RowLayout { } } - - InnerBorder { - leftThickness: Appearance.padding.normal / 2 } } } -- cgit v1.2.3-freya From 7023a72ef1ad21b96aae1bbf820f2181df837f9b Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 17:07:27 -0500 Subject: controlcenter: corrected wallpaper flicker issue --- .../controlcenter/appearance/AppearancePane.qml | 79 +++++++++++----------- modules/launcher/items/WallpaperItem.qml | 1 + shell | 1 + 3 files changed, 42 insertions(+), 39 deletions(-) create mode 160000 shell diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index ba95977..baf49cf 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -1049,10 +1049,10 @@ RowLayout { Layout.preferredHeight: 140 Layout.minimumWidth: 200 Layout.minimumHeight: 140 + Layout.maximumWidth: 200 + Layout.maximumHeight: 140 readonly property bool isCurrent: modelData.path === Wallpapers.actualCurrent - readonly property real imageWidth: Math.max(1, width) - readonly property real imageHeight: Math.max(1, height) StateLayer { radius: Appearance.rounding.normal @@ -1069,71 +1069,72 @@ RowLayout { color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.normal - border.width: isCurrent ? 2 : 0 - border.color: Colours.palette.m3primary - CachingImage { id: cachingImage path: modelData.path anchors.fill: parent + cache: true + visible: opacity > 0 - // Ensure sourceSize is always set to valid dimensions - sourceSize: Qt.size(Math.max(1, Math.floor(parent.width)), Math.max(1, Math.floor(parent.height))) - - // Show when ready, hide if fallback is showing - opacity: status === Image.Ready && !fallbackImage.visible ? 1 : 0 + // Show when ready + opacity: status === Image.Ready ? 1 : 0 Behavior on opacity { NumberAnimation { - duration: 200 + duration: 150 + easing.type: Easing.OutQuad } } } - // Fallback: Direct image load if caching fails or is slow + // Fallback image for when caching fails Image { id: fallbackImage anchors.fill: parent - source: modelData.path + source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : "" asynchronous: true fillMode: Image.PreserveAspectCrop - sourceSize: Qt.size(Math.max(1, Math.floor(parent.width)), Math.max(1, Math.floor(parent.height))) - - // Show if caching image hasn't loaded after a delay + cache: true visible: opacity > 0 - opacity: 0 - - Timer { - id: fallbackTimer - interval: 500 - running: cachingImage.status === Image.Loading || (cachingImage.status !== Image.Ready && cachingImage.status !== Image.Null) - onTriggered: { - if (cachingImage.status !== Image.Ready && fallbackImage.status === Image.Ready) { - fallbackImage.opacity = 1; - } - } - } - // Also check status changes - onStatusChanged: { - if (status === Image.Ready && cachingImage.status !== Image.Ready) { - Qt.callLater(() => { - if (cachingImage.status !== Image.Ready) { - fallbackImage.opacity = 1; - } - }); - } - } + opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 Behavior on opacity { NumberAnimation { - duration: 200 + duration: 150 + easing.type: Easing.OutQuad } } } + // Timer to trigger fallback only if caching hasn't loaded + Timer { + id: fallbackTimer + + property bool triggered: false + interval: 800 + running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null + onTriggered: triggered = true + } + } + + // Border overlay that doesn't affect image size + Rectangle { + anchors.fill: parent + color: "transparent" + radius: Appearance.rounding.normal + border.width: isCurrent ? 2 : 0 + border.color: Colours.palette.m3primary + + Behavior on border.width { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + MaterialIcon { anchors.right: parent.right anchors.top: parent.top diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 1128bad..9fdac3f 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -67,6 +67,7 @@ Item { CachingImage { path: root.modelData.path smooth: !root.PathView.view.moving + cache: true anchors.fill: parent } diff --git a/shell b/shell new file mode 160000 index 0000000..442b49e --- /dev/null +++ b/shell @@ -0,0 +1 @@ +Subproject commit 442b49e0e1975d51efcef90d10566c90dedbf7e1 -- cgit v1.2.3-freya From 41b1551d3b63b2d1762008eefcaaff93f50d572f Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 17:08:18 -0500 Subject: deleted: shell --- shell | 1 - 1 file changed, 1 deletion(-) delete mode 160000 shell diff --git a/shell b/shell deleted file mode 160000 index 442b49e..0000000 --- a/shell +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 442b49e0e1975d51efcef90d10566c90dedbf7e1 -- cgit v1.2.3-freya From 4ada7ea608711aa21f8bfc51698e7a3ec261aeeb Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 17:16:52 -0500 Subject: controlcenter: corrected certain containers not respecting transparency layers --- components/SectionContainer.qml | 2 +- components/controls/FilledSlider.qml | 2 +- components/controls/SpinBoxRow.qml | 2 +- components/controls/StyledSlider.qml | 2 +- components/controls/SwitchRow.qml | 2 +- modules/controlcenter/audio/AudioPane.qml | 4 ++-- modules/controlcenter/launcher/LauncherPane.qml | 4 ++-- modules/launcher/Content.qml | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml index d41254b..f7dfef4 100644 --- a/components/SectionContainer.qml +++ b/components/SectionContainer.qml @@ -15,7 +15,7 @@ StyledRect { implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) ColumnLayout { id: contentColumn diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml index 78b8a5c..80dd44c 100644 --- a/components/controls/FilledSlider.qml +++ b/components/controls/FilledSlider.qml @@ -15,7 +15,7 @@ Slider { orientation: Qt.Vertical background: StyledRect { - color: Colours.tPalette.m3surfaceContainer + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.full StyledRect { diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml index 2507b80..4902627 100644 --- a/components/controls/SpinBoxRow.qml +++ b/components/controls/SpinBoxRow.qml @@ -19,7 +19,7 @@ StyledRect { Layout.fillWidth: true implicitHeight: row.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { Anim {} diff --git a/components/controls/StyledSlider.qml b/components/controls/StyledSlider.qml index 92c8aa8..55e8c8d 100644 --- a/components/controls/StyledSlider.qml +++ b/components/controls/StyledSlider.qml @@ -32,7 +32,7 @@ Slider { implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6 - color: Colours.tPalette.m3surfaceContainer + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.full topLeftRadius: root.implicitHeight / 15 bottomLeftRadius: root.implicitHeight / 15 diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml index 0ec7aa5..7fa3e1b 100644 --- a/components/controls/SwitchRow.qml +++ b/components/controls/SwitchRow.qml @@ -17,7 +17,7 @@ StyledRect { Layout.fillWidth: true implicitHeight: row.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { Anim {} diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 005de3a..1c0c770 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -127,7 +127,7 @@ RowLayout { Layout.fillWidth: true - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Audio.sink?.id === modelData.id ? Colours.tPalette.m3surfaceContainer.a : 0) + color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal StateLayer { @@ -205,7 +205,7 @@ RowLayout { Layout.fillWidth: true - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Audio.source?.id === modelData.id ? Colours.tPalette.m3surfaceContainer.a : 0) + color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal StateLayer { diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 12abc1e..dc7a7a7 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -218,7 +218,7 @@ RowLayout { Layout.topMargin: Appearance.spacing.normal Layout.bottomMargin: Appearance.spacing.small - color: Colours.tPalette.m3surfaceContainer + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.full implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight) @@ -317,7 +317,7 @@ RowLayout { readonly property bool isSelected: root.selectedApp === modelData - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isSelected ? Colours.tPalette.m3surfaceContainer.a : 0) + color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal StateLayer { diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index f674569..c085976 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -47,7 +47,7 @@ Item { StyledRect { id: searchWrapper - color: Colours.tPalette.m3surfaceContainer + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.full anchors.left: parent.left -- cgit v1.2.3-freya From ec8da25d194eb6c9d0898870e52b061853907583 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 19:07:30 -0500 Subject: controlcenter: revamped and added more sliders --- .../controlcenter/appearance/AppearancePane.qml | 1043 ++++++++++++++++++-- modules/controlcenter/audio/AudioPane.qml | 156 ++- modules/controlcenter/taskbar/TaskbarPane.qml | 104 +- 3 files changed, 1199 insertions(+), 104 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index baf49cf..ea81fa7 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -431,15 +431,102 @@ RowLayout { sidebarFlickable.collapseAllSections(animationsSection); } - SpinBoxRow { - label: qsTr("Animation duration scale") - min: 0.1 - max: 5 - step: 0.1 - value: rootPane.animDurationsScale - onValueModified: value => { - rootPane.animDurationsScale = value; - rootPane.saveConfig(); + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Animation duration scale") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: animDurationsInput.implicitHeight + Appearance.padding.small * 2 + color: animDurationsInputHover.containsMouse || animDurationsInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: animDurationsInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: animDurationsInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: animDurationsInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0.1; top: 5.0 } + + Component.onCompleted: { + text = (rootPane.animDurationsScale).toFixed(1); + } + + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 5.0) { + rootPane.animDurationsScale = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 5.0) { + text = (rootPane.animDurationsScale).toFixed(1); + } + } + } + } + + StyledText { + text: "×" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: animDurationsSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0.1 + to: 5.0 + value: rootPane.animDurationsScale + onMoved: { + rootPane.animDurationsScale = animDurationsSlider.value; + if (!animDurationsInput.activeFocus) { + animDurationsInput.text = (animDurationsSlider.value).toFixed(1); + } + rootPane.saveConfig(); + } + } } } } @@ -473,8 +560,7 @@ RowLayout { delegate: StyledRect { required property string modelData - anchors.left: parent.left - anchors.right: parent.right + width: parent ? parent.width : 0 readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) @@ -546,8 +632,7 @@ RowLayout { delegate: StyledRect { required property string modelData - anchors.left: parent.left - anchors.right: parent.right + width: parent ? parent.width : 0 readonly property bool isCurrent: modelData === rootPane.fontFamilyMono color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) @@ -619,8 +704,7 @@ RowLayout { delegate: StyledRect { required property string modelData - anchors.left: parent.left - anchors.right: parent.right + width: parent ? parent.width : 0 readonly property bool isCurrent: modelData === rootPane.fontFamilySans color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) @@ -670,15 +754,102 @@ RowLayout { } } - SpinBoxRow { - label: qsTr("Font size scale") - min: 0.1 - max: 5 - step: 0.1 - value: rootPane.fontSizeScale - onValueModified: value => { - rootPane.fontSizeScale = value; - rootPane.saveConfig(); + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Font size scale") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: fontSizeInput.implicitHeight + Appearance.padding.small * 2 + color: fontSizeInputHover.containsMouse || fontSizeInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: fontSizeInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: fontSizeInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: fontSizeInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0.1; top: 5.0 } + + Component.onCompleted: { + text = (rootPane.fontSizeScale).toFixed(1); + } + + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 5.0) { + rootPane.fontSizeScale = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 5.0) { + text = (rootPane.fontSizeScale).toFixed(1); + } + } + } + } + + StyledText { + text: "×" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: fontSizeSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0.1 + to: 5.0 + value: rootPane.fontSizeScale + onMoved: { + rootPane.fontSizeScale = fontSizeSlider.value; + if (!fontSizeInput.activeFocus) { + fontSizeInput.text = (fontSizeSlider.value).toFixed(1); + } + rootPane.saveConfig(); + } + } } } } @@ -690,39 +861,300 @@ RowLayout { sidebarFlickable.collapseAllSections(scalesSection); } - SpinBoxRow { - label: qsTr("Padding scale") - min: 0.1 - max: 5 - step: 0.1 - value: rootPane.paddingScale - onValueModified: value => { - rootPane.paddingScale = value; - rootPane.saveConfig(); + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Padding scale") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: paddingInput.implicitHeight + Appearance.padding.small * 2 + color: paddingInputHover.containsMouse || paddingInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: paddingInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: paddingInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: paddingInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0.1; top: 5.0 } + + Component.onCompleted: { + text = (rootPane.paddingScale).toFixed(1); + } + + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 5.0) { + rootPane.paddingScale = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 5.0) { + text = (rootPane.paddingScale).toFixed(1); + } + } + } + } + + StyledText { + text: "×" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: paddingSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0.1 + to: 5.0 + value: rootPane.paddingScale + onMoved: { + rootPane.paddingScale = paddingSlider.value; + if (!paddingInput.activeFocus) { + paddingInput.text = (paddingSlider.value).toFixed(1); + } + rootPane.saveConfig(); + } + } } } - SpinBoxRow { - label: qsTr("Rounding scale") - min: 0.1 - max: 5 - step: 0.1 - value: rootPane.roundingScale - onValueModified: value => { - rootPane.roundingScale = value; - rootPane.saveConfig(); + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Rounding scale") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: roundingInput.implicitHeight + Appearance.padding.small * 2 + color: roundingInputHover.containsMouse || roundingInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: roundingInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: roundingInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: roundingInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0.1; top: 5.0 } + + Component.onCompleted: { + text = (rootPane.roundingScale).toFixed(1); + } + + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 5.0) { + rootPane.roundingScale = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 5.0) { + text = (rootPane.roundingScale).toFixed(1); + } + } + } + } + + StyledText { + text: "×" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: roundingSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0.1 + to: 5.0 + value: rootPane.roundingScale + onMoved: { + rootPane.roundingScale = roundingSlider.value; + if (!roundingInput.activeFocus) { + roundingInput.text = (roundingSlider.value).toFixed(1); + } + rootPane.saveConfig(); + } + } } } - SpinBoxRow { - label: qsTr("Spacing scale") - min: 0.1 - max: 5 - step: 0.1 - value: rootPane.spacingScale - onValueModified: value => { - rootPane.spacingScale = value; - rootPane.saveConfig(); + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Spacing scale") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: spacingInput.implicitHeight + Appearance.padding.small * 2 + color: spacingInputHover.containsMouse || spacingInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: spacingInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: spacingInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: spacingInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0.1; top: 5.0 } + + Component.onCompleted: { + text = (rootPane.spacingScale).toFixed(1); + } + + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 5.0) { + rootPane.spacingScale = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 5.0) { + text = (rootPane.spacingScale).toFixed(1); + } + } + } + } + + StyledText { + text: "×" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: spacingSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0.1 + to: 5.0 + value: rootPane.spacingScale + onMoved: { + rootPane.spacingScale = spacingSlider.value; + if (!spacingInput.activeFocus) { + spacingInput.text = (spacingSlider.value).toFixed(1); + } + rootPane.saveConfig(); + } + } } } } @@ -763,8 +1195,60 @@ RowLayout { Layout.fillWidth: true } + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: transparencyBaseInput.implicitHeight + Appearance.padding.small * 2 + color: transparencyBaseInputHover.containsMouse || transparencyBaseInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: transparencyBaseInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: transparencyBaseInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: transparencyBaseInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 100 } + + Component.onCompleted: { + text = Math.round(rootPane.transparencyBase * 100).toString(); + } + + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + rootPane.transparencyBase = val / 100; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(rootPane.transparencyBase * 100).toString(); + } + } + } + } + StyledText { - text: qsTr("%1%").arg(Math.round(rootPane.transparencyBase * 100)) + text: "%" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal } @@ -781,6 +1265,9 @@ RowLayout { value: rootPane.transparencyBase * 100 onMoved: { rootPane.transparencyBase = baseSlider.value / 100; + if (!transparencyBaseInput.activeFocus) { + transparencyBaseInput.text = Math.round(baseSlider.value).toString(); + } rootPane.saveConfig(); } } @@ -807,8 +1294,60 @@ RowLayout { Layout.fillWidth: true } + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: transparencyLayersInput.implicitHeight + Appearance.padding.small * 2 + color: transparencyLayersInputHover.containsMouse || transparencyLayersInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: transparencyLayersInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: transparencyLayersInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: transparencyLayersInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 100 } + + Component.onCompleted: { + text = Math.round(rootPane.transparencyLayers * 100).toString(); + } + + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + rootPane.transparencyLayers = val / 100; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(rootPane.transparencyLayers * 100).toString(); + } + } + } + } + StyledText { - text: qsTr("%1%").arg(Math.round(rootPane.transparencyLayers * 100)) + text: "%" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal } @@ -825,6 +1364,9 @@ RowLayout { value: rootPane.transparencyLayers * 100 onMoved: { rootPane.transparencyLayers = layersSlider.value / 100; + if (!transparencyLayersInput.activeFocus) { + transparencyLayersInput.text = Math.round(layersSlider.value).toString(); + } rootPane.saveConfig(); } } @@ -839,27 +1381,189 @@ RowLayout { sidebarFlickable.collapseAllSections(borderSection); } - SpinBoxRow { - label: qsTr("Border rounding") - min: 0.1 - max: 100 - step: 0.1 - value: rootPane.borderRounding - onValueModified: value => { - rootPane.borderRounding = value; - rootPane.saveConfig(); + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Border rounding") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: borderRoundingInput.implicitHeight + Appearance.padding.small * 2 + color: borderRoundingInputHover.containsMouse || borderRoundingInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: borderRoundingInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: borderRoundingInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: borderRoundingInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0.1; top: 100 } + + Component.onCompleted: { + text = (rootPane.borderRounding).toFixed(1); + } + + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 100) { + rootPane.borderRounding = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 100) { + text = (rootPane.borderRounding).toFixed(1); + } + } + } + } + } + + StyledSlider { + id: borderRoundingSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0.1 + to: 100 + value: rootPane.borderRounding + onMoved: { + rootPane.borderRounding = borderRoundingSlider.value; + if (!borderRoundingInput.activeFocus) { + borderRoundingInput.text = (borderRoundingSlider.value).toFixed(1); + } + rootPane.saveConfig(); + } + } } } - SpinBoxRow { - label: qsTr("Border thickness") - min: 0.1 - max: 100 - step: 0.1 - value: rootPane.borderThickness - onValueModified: value => { - rootPane.borderThickness = value; - rootPane.saveConfig(); + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Border thickness") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: borderThicknessInput.implicitHeight + Appearance.padding.small * 2 + color: borderThicknessInputHover.containsMouse || borderThicknessInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: borderThicknessInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: borderThicknessInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: borderThicknessInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0.1; top: 100 } + + Component.onCompleted: { + text = (rootPane.borderThickness).toFixed(1); + } + + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 100) { + rootPane.borderThickness = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 100) { + text = (rootPane.borderThickness).toFixed(1); + } + } + } + } + } + + StyledSlider { + id: borderThicknessSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0.1 + to: 100 + value: rootPane.borderThickness + onMoved: { + rootPane.borderThickness = borderThicknessSlider.value; + if (!borderThicknessInput.activeFocus) { + borderThicknessInput.text = (borderThicknessSlider.value).toFixed(1); + } + rootPane.saveConfig(); + } + } } } } @@ -914,25 +1618,190 @@ RowLayout { } } - SpinBoxRow { - label: qsTr("Visualiser rounding") - min: 0 - max: 10 - value: Math.round(rootPane.visualiserRounding) - onValueModified: value => { - rootPane.visualiserRounding = value; - rootPane.saveConfig(); + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Visualiser rounding") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: visualiserRoundingInput.implicitHeight + Appearance.padding.small * 2 + color: visualiserRoundingInputHover.containsMouse || visualiserRoundingInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: visualiserRoundingInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: visualiserRoundingInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: visualiserRoundingInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 10 } + + Component.onCompleted: { + text = Math.round(rootPane.visualiserRounding).toString(); + } + + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 10) { + rootPane.visualiserRounding = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 10) { + text = Math.round(rootPane.visualiserRounding).toString(); + } + } + } + } + } + + StyledSlider { + id: visualiserRoundingSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0 + to: 10 + stepSize: 1 + value: rootPane.visualiserRounding + onMoved: { + rootPane.visualiserRounding = Math.round(visualiserRoundingSlider.value); + if (!visualiserRoundingInput.activeFocus) { + visualiserRoundingInput.text = Math.round(visualiserRoundingSlider.value).toString(); + } + rootPane.saveConfig(); + } + } } } - SpinBoxRow { - label: qsTr("Visualiser spacing") - min: 0 - max: 10 - value: Math.round(rootPane.visualiserSpacing) - onValueModified: value => { - rootPane.visualiserSpacing = value; - rootPane.saveConfig(); + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Visualiser spacing") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: visualiserSpacingInput.implicitHeight + Appearance.padding.small * 2 + color: visualiserSpacingInputHover.containsMouse || visualiserSpacingInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: visualiserSpacingInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: visualiserSpacingInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: visualiserSpacingInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0; top: 2 } + + Component.onCompleted: { + text = (rootPane.visualiserSpacing).toFixed(1); + } + + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0 && val <= 2) { + rootPane.visualiserSpacing = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0 || val > 2) { + text = (rootPane.visualiserSpacing).toFixed(1); + } + } + } + } + } + + StyledSlider { + id: visualiserSpacingSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0 + to: 2 + value: rootPane.visualiserSpacing + onMoved: { + rootPane.visualiserSpacing = visualiserSpacingSlider.value; + if (!visualiserSpacingInput.activeFocus) { + visualiserSpacingInput.text = (visualiserSpacingSlider.value).toFixed(1); + } + rootPane.saveConfig(); + } + } } } } diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 1c0c770..3440a2f 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -336,11 +336,74 @@ RowLayout { 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: Audio.muted ? qsTr("Muted") : qsTr("%1%").arg(Math.round(Audio.volume * 100)) - color: Audio.muted ? Colours.palette.m3primary : Colours.palette.m3outline + text: "%" + color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal - font.weight: 500 + opacity: Audio.muted ? 0.5 : 1 } StyledRect { @@ -362,20 +425,26 @@ RowLayout { id: muteIcon anchors.centerIn: parent - text: Audio.muted ? "volume_off" : "volume_mute" + 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) + onMoved: { + Audio.setVolume(value); + if (!outputVolumeInput.activeFocus) { + outputVolumeInput.text = Math.round(value * 100).toString(); + } + } } } } @@ -406,11 +475,74 @@ RowLayout { 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: Audio.sourceMuted ? qsTr("Muted") : qsTr("%1%").arg(Math.round(Audio.sourceVolume * 100)) - color: Audio.sourceMuted ? Colours.palette.m3primary : Colours.palette.m3outline + text: "%" + color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal - font.weight: 500 + opacity: Audio.sourceMuted ? 0.5 : 1 } StyledRect { @@ -439,13 +571,19 @@ RowLayout { } 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) + onMoved: { + Audio.setSourceVolume(value); + if (!inputVolumeInput.activeFocus) { + inputVolumeInput.text = Math.round(value * 100).toString(); + } + } } } } diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 85e5275..6d9761b 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -239,14 +239,102 @@ RowLayout { } } - SpinBoxRow { - label: qsTr("Drag threshold") - min: 0 - max: 100 - value: root.dragThreshold - onValueModified: value => { - root.dragThreshold = value; - root.saveConfig(); + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Drag threshold") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: dragThresholdInput.implicitHeight + Appearance.padding.small * 2 + color: dragThresholdInputHover.containsMouse || dragThresholdInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: dragThresholdInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: dragThresholdInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: dragThresholdInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 100 } + + Component.onCompleted: { + text = root.dragThreshold.toString(); + } + + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + root.dragThreshold = val; + root.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = root.dragThreshold.toString(); + } + } + } + } + + StyledText { + text: "px" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: dragThresholdSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0 + to: 100 + value: root.dragThreshold + onMoved: { + root.dragThreshold = Math.round(dragThresholdSlider.value); + if (!dragThresholdInput.activeFocus) { + dragThresholdInput.text = Math.round(dragThresholdSlider.value).toString(); + } + root.saveConfig(); + } + } } } } -- cgit v1.2.3-freya From fc2640c1ef3f18b5a7d9a1c6b2e427b9acdf93da Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 19:08:52 -0500 Subject: controlcenter: minor warning fix --- modules/controlcenter/bluetooth/Details.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index cbccd9b..445f568 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -35,7 +35,7 @@ Item { MaterialIcon { Layout.alignment: Qt.AlignHCenter animate: true - text: Icons.getBluetoothIcon(root.device.icon) + text: Icons.getBluetoothIcon(root.device?.icon ?? "") font.pointSize: Appearance.font.size.extraLarge * 3 font.bold: true } -- cgit v1.2.3-freya From b5a31fbc59d6d2408a0cc6aaa3ea31c348a7241d Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 19:15:34 -0500 Subject: controlcenter: launcher pane now fades in applications --- modules/controlcenter/launcher/LauncherPane.qml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index dc7a7a7..a6f6bd6 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -320,6 +320,19 @@ RowLayout { color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal + opacity: 0 + + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + + Component.onCompleted: { + opacity = 1; + } + StateLayer { function onClicked(): void { root.selectedApp = modelData; -- cgit v1.2.3-freya From 7db0348ff97ecff2e258ace65aeb95f0eafe508b Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 19:20:01 -0500 Subject: controlcenter: forget network button color --- modules/controlcenter/network/WirelessDetails.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 651f1fb..82ac975 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -151,8 +151,8 @@ Item { } return Nmcli.hasSavedProfile(root.network.ssid); } - inactiveColour: Colours.palette.m3errorContainer - inactiveOnColour: Colours.palette.m3onErrorContainer + inactiveColour: Colours.palette.m3secondaryContainer + inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Forget Network") onClicked: { -- cgit v1.2.3-freya From 5be8d68ae7bd1f58ed10979a9b1c7cf055519fc6 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 19:26:14 -0500 Subject: controlcenter: increased animation on fade-in (wallpaper and applications) --- .../controlcenter/appearance/AppearancePane.qml | 44 +++++++++++----------- modules/controlcenter/launcher/LauncherPane.qml | 2 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index ea81fa7..388fc88 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -518,13 +518,13 @@ RowLayout { from: 0.1 to: 5.0 - value: rootPane.animDurationsScale + value: rootPane.animDurationsScale onMoved: { rootPane.animDurationsScale = animDurationsSlider.value; if (!animDurationsInput.activeFocus) { animDurationsInput.text = (animDurationsSlider.value).toFixed(1); } - rootPane.saveConfig(); + rootPane.saveConfig(); } } } @@ -841,13 +841,13 @@ RowLayout { from: 0.1 to: 5.0 - value: rootPane.fontSizeScale + value: rootPane.fontSizeScale onMoved: { rootPane.fontSizeScale = fontSizeSlider.value; if (!fontSizeInput.activeFocus) { fontSizeInput.text = (fontSizeSlider.value).toFixed(1); } - rootPane.saveConfig(); + rootPane.saveConfig(); } } } @@ -948,13 +948,13 @@ RowLayout { from: 0.1 to: 5.0 - value: rootPane.paddingScale + value: rootPane.paddingScale onMoved: { rootPane.paddingScale = paddingSlider.value; if (!paddingInput.activeFocus) { paddingInput.text = (paddingSlider.value).toFixed(1); } - rootPane.saveConfig(); + rootPane.saveConfig(); } } } @@ -1047,13 +1047,13 @@ RowLayout { from: 0.1 to: 5.0 - value: rootPane.roundingScale + value: rootPane.roundingScale onMoved: { rootPane.roundingScale = roundingSlider.value; if (!roundingInput.activeFocus) { roundingInput.text = (roundingSlider.value).toFixed(1); } - rootPane.saveConfig(); + rootPane.saveConfig(); } } } @@ -1146,13 +1146,13 @@ RowLayout { from: 0.1 to: 5.0 - value: rootPane.spacingScale + value: rootPane.spacingScale onMoved: { rootPane.spacingScale = spacingSlider.value; if (!spacingInput.activeFocus) { spacingInput.text = (spacingSlider.value).toFixed(1); } - rootPane.saveConfig(); + rootPane.saveConfig(); } } } @@ -1462,13 +1462,13 @@ RowLayout { from: 0.1 to: 100 - value: rootPane.borderRounding + value: rootPane.borderRounding onMoved: { rootPane.borderRounding = borderRoundingSlider.value; if (!borderRoundingInput.activeFocus) { borderRoundingInput.text = (borderRoundingSlider.value).toFixed(1); } - rootPane.saveConfig(); + rootPane.saveConfig(); } } } @@ -1555,13 +1555,13 @@ RowLayout { from: 0.1 to: 100 - value: rootPane.borderThickness + value: rootPane.borderThickness onMoved: { rootPane.borderThickness = borderThicknessSlider.value; if (!borderThicknessInput.activeFocus) { borderThicknessInput.text = (borderThicknessSlider.value).toFixed(1); } - rootPane.saveConfig(); + rootPane.saveConfig(); } } } @@ -1677,7 +1677,7 @@ RowLayout { const val = parseInt(text); if (!isNaN(val) && val >= 0 && val <= 10) { rootPane.visualiserRounding = val; - rootPane.saveConfig(); + rootPane.saveConfig(); } } } @@ -1706,11 +1706,11 @@ RowLayout { if (!visualiserRoundingInput.activeFocus) { visualiserRoundingInput.text = Math.round(visualiserRoundingSlider.value).toString(); } - rootPane.saveConfig(); - } - } + rootPane.saveConfig(); } } + } + } SectionContainer { contentSpacing: Appearance.spacing.normal @@ -1951,7 +1951,7 @@ RowLayout { Behavior on opacity { NumberAnimation { - duration: 150 + duration: 1000 easing.type: Easing.OutQuad } } @@ -1972,7 +1972,7 @@ RowLayout { Behavior on opacity { NumberAnimation { - duration: 150 + duration: 1000 easing.type: Easing.OutQuad } } @@ -2049,7 +2049,7 @@ RowLayout { Behavior on opacity { NumberAnimation { - duration: 200 + duration: 1000 easing.type: Easing.OutCubic } } @@ -2085,7 +2085,7 @@ RowLayout { Behavior on opacity { NumberAnimation { - duration: 200 + duration: 1000 easing.type: Easing.OutCubic } } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index a6f6bd6..a7e122e 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -324,7 +324,7 @@ RowLayout { Behavior on opacity { NumberAnimation { - duration: 200 + duration: 1000 easing.type: Easing.OutCubic } } -- cgit v1.2.3-freya From 041be54c69b1c9a2fe5eacca7d765a9ccc37aad6 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 19:31:26 -0500 Subject: controlcenter: animation disappearing glitch fix --- modules/controlcenter/Panes.qml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 52ad7f2..77b2fd2 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -88,15 +88,13 @@ ClippingRectangle { id: loader anchors.fill: parent - clip: true + clip: false asynchronous: true active: { - if (root.session.activeIndex === pane.index) - return true; - - const ly = -layout.y; - const ty = pane.index * root.height; - return ly + root.height > ty && ly < ty + root.height; + // Keep loaders active for current and adjacent panels + // This prevents content from disappearing during panel transitions + const diff = Math.abs(root.session.activeIndex - pane.index); + return diff <= 1; } } } -- cgit v1.2.3-freya From cc74308800af5146791e953ab25a270fec50fdf3 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 19:40:19 -0500 Subject: config: added save to singleton, updated relevant settings panes --- config/Config.qml | 381 ++++++++++++++++++++- .../controlcenter/appearance/AppearancePane.qml | 3 + 2 files changed, 382 insertions(+), 2 deletions(-) diff --git a/config/Config.qml b/config/Config.qml index 267a184..3756043 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -4,6 +4,7 @@ import qs.utils import Caelestia import Quickshell import Quickshell.Io +import QtQuick Singleton { id: root @@ -26,16 +27,392 @@ Singleton { property alias services: adapter.services property alias paths: adapter.paths + // Public save function - call this to persist config changes + function save(): void { + saveTimer.restart(); + } + ElapsedTimer { id: timer } + Timer { + id: saveTimer + + interval: 500 + onTriggered: { + try { + // Parse current config to preserve structure and comments if possible + let config = {}; + try { + config = JSON.parse(fileView.text()); + } catch (e) { + // If parsing fails, start with empty object + config = {}; + } + + // Update config with current values + config = serializeConfig(); + + // Save to file with pretty printing + fileView.setText(JSON.stringify(config, null, 2)); + } catch (e) { + Toaster.toast(qsTr("Failed to serialize config"), e.message, "settings_alert", Toast.Error); + } + } + } + + // Helper function to serialize the config object + function serializeConfig(): var { + return { + appearance: serializeAppearance(), + general: serializeGeneral(), + background: serializeBackground(), + bar: serializeBar(), + border: serializeBorder(), + dashboard: serializeDashboard(), + controlCenter: serializeControlCenter(), + launcher: serializeLauncher(), + notifs: serializeNotifs(), + osd: serializeOsd(), + session: serializeSession(), + winfo: serializeWinfo(), + lock: serializeLock(), + utilities: serializeUtilities(), + sidebar: serializeSidebar(), + services: serializeServices(), + paths: serializePaths() + }; + } + + function serializeAppearance(): var { + return { + rounding: { scale: appearance.rounding.scale }, + spacing: { scale: appearance.spacing.scale }, + padding: { scale: appearance.padding.scale }, + font: { + family: { + sans: appearance.font.family.sans, + mono: appearance.font.family.mono, + material: appearance.font.family.material, + clock: appearance.font.family.clock + }, + size: { scale: appearance.font.size.scale } + }, + anim: { + durations: { scale: appearance.anim.durations.scale } + }, + transparency: { + enabled: appearance.transparency.enabled, + base: appearance.transparency.base, + layers: appearance.transparency.layers + } + }; + } + + function serializeGeneral(): var { + return { + apps: { + terminal: general.apps.terminal, + audio: general.apps.audio, + playback: general.apps.playback, + explorer: general.apps.explorer + }, + idle: { + lockBeforeSleep: general.idle.lockBeforeSleep, + inhibitWhenAudio: general.idle.inhibitWhenAudio, + timeouts: general.idle.timeouts + }, + battery: { + warnLevels: general.battery.warnLevels, + criticalLevel: general.battery.criticalLevel + } + }; + } + + function serializeBackground(): var { + return { + enabled: background.enabled, + desktopClock: { + enabled: background.desktopClock.enabled + }, + visualiser: { + enabled: background.visualiser.enabled, + autoHide: background.visualiser.autoHide, + blur: background.visualiser.blur, + rounding: background.visualiser.rounding, + spacing: background.visualiser.spacing + } + }; + } + + function serializeBar(): var { + return { + persistent: bar.persistent, + showOnHover: bar.showOnHover, + dragThreshold: bar.dragThreshold, + scrollActions: { + workspaces: bar.scrollActions.workspaces, + volume: bar.scrollActions.volume, + brightness: bar.scrollActions.brightness + }, + popouts: { + activeWindow: bar.popouts.activeWindow, + tray: bar.popouts.tray, + statusIcons: bar.popouts.statusIcons + }, + workspaces: { + shown: bar.workspaces.shown, + activeIndicator: bar.workspaces.activeIndicator, + occupiedBg: bar.workspaces.occupiedBg, + showWindows: bar.workspaces.showWindows, + showWindowsOnSpecialWorkspaces: bar.workspaces.showWindowsOnSpecialWorkspaces, + activeTrail: bar.workspaces.activeTrail, + perMonitorWorkspaces: bar.workspaces.perMonitorWorkspaces, + label: bar.workspaces.label, + occupiedLabel: bar.workspaces.occupiedLabel, + activeLabel: bar.workspaces.activeLabel, + capitalisation: bar.workspaces.capitalisation, + specialWorkspaceIcons: bar.workspaces.specialWorkspaceIcons + }, + tray: { + background: bar.tray.background, + recolour: bar.tray.recolour, + compact: bar.tray.compact, + iconSubs: bar.tray.iconSubs + }, + status: { + showAudio: bar.status.showAudio, + showMicrophone: bar.status.showMicrophone, + showKbLayout: bar.status.showKbLayout, + showNetwork: bar.status.showNetwork, + showBluetooth: bar.status.showBluetooth, + showBattery: bar.status.showBattery, + showLockStatus: bar.status.showLockStatus + }, + clock: { + showIcon: bar.clock.showIcon + }, + sizes: { + innerWidth: bar.sizes.innerWidth, + windowPreviewSize: bar.sizes.windowPreviewSize, + trayMenuWidth: bar.sizes.trayMenuWidth, + batteryWidth: bar.sizes.batteryWidth, + networkWidth: bar.sizes.networkWidth + }, + entries: bar.entries + }; + } + + function serializeBorder(): var { + return { + thickness: border.thickness, + rounding: border.rounding + }; + } + + function serializeDashboard(): var { + return { + enabled: dashboard.enabled, + showOnHover: dashboard.showOnHover, + mediaUpdateInterval: dashboard.mediaUpdateInterval, + dragThreshold: dashboard.dragThreshold, + sizes: { + tabIndicatorHeight: dashboard.sizes.tabIndicatorHeight, + tabIndicatorSpacing: dashboard.sizes.tabIndicatorSpacing, + infoWidth: dashboard.sizes.infoWidth, + infoIconSize: dashboard.sizes.infoIconSize, + dateTimeWidth: dashboard.sizes.dateTimeWidth, + mediaWidth: dashboard.sizes.mediaWidth, + mediaProgressSweep: dashboard.sizes.mediaProgressSweep, + mediaProgressThickness: dashboard.sizes.mediaProgressThickness, + resourceProgessThickness: dashboard.sizes.resourceProgessThickness, + weatherWidth: dashboard.sizes.weatherWidth, + mediaCoverArtSize: dashboard.sizes.mediaCoverArtSize, + mediaVisualiserSize: dashboard.sizes.mediaVisualiserSize, + resourceSize: dashboard.sizes.resourceSize + } + }; + } + + function serializeControlCenter(): var { + return { + sizes: { + heightMult: controlCenter.sizes.heightMult, + ratio: controlCenter.sizes.ratio + } + }; + } + + function serializeLauncher(): var { + return { + enabled: launcher.enabled, + showOnHover: launcher.showOnHover, + maxShown: launcher.maxShown, + maxWallpapers: launcher.maxWallpapers, + specialPrefix: launcher.specialPrefix, + actionPrefix: launcher.actionPrefix, + enableDangerousActions: launcher.enableDangerousActions, + dragThreshold: launcher.dragThreshold, + vimKeybinds: launcher.vimKeybinds, + hiddenApps: launcher.hiddenApps, + useFuzzy: { + apps: launcher.useFuzzy.apps, + actions: launcher.useFuzzy.actions, + schemes: launcher.useFuzzy.schemes, + variants: launcher.useFuzzy.variants, + wallpapers: launcher.useFuzzy.wallpapers + }, + sizes: { + itemWidth: launcher.sizes.itemWidth, + itemHeight: launcher.sizes.itemHeight, + wallpaperWidth: launcher.sizes.wallpaperWidth, + wallpaperHeight: launcher.sizes.wallpaperHeight + }, + actions: launcher.actions + }; + } + + function serializeNotifs(): var { + return { + expire: notifs.expire, + defaultExpireTimeout: notifs.defaultExpireTimeout, + clearThreshold: notifs.clearThreshold, + expandThreshold: notifs.expandThreshold, + actionOnClick: notifs.actionOnClick, + groupPreviewNum: notifs.groupPreviewNum, + sizes: { + width: notifs.sizes.width, + image: notifs.sizes.image, + badge: notifs.sizes.badge + } + }; + } + + function serializeOsd(): var { + return { + enabled: osd.enabled, + hideDelay: osd.hideDelay, + enableBrightness: osd.enableBrightness, + enableMicrophone: osd.enableMicrophone, + sizes: { + sliderWidth: osd.sizes.sliderWidth, + sliderHeight: osd.sizes.sliderHeight + } + }; + } + + function serializeSession(): var { + return { + enabled: session.enabled, + dragThreshold: session.dragThreshold, + vimKeybinds: session.vimKeybinds, + commands: { + logout: session.commands.logout, + shutdown: session.commands.shutdown, + hibernate: session.commands.hibernate, + reboot: session.commands.reboot + }, + sizes: { + button: session.sizes.button + } + }; + } + + function serializeWinfo(): var { + return { + sizes: { + heightMult: winfo.sizes.heightMult, + detailsWidth: winfo.sizes.detailsWidth + } + }; + } + + function serializeLock(): var { + return { + recolourLogo: lock.recolourLogo, + enableFprint: lock.enableFprint, + maxFprintTries: lock.maxFprintTries, + sizes: { + heightMult: lock.sizes.heightMult, + ratio: lock.sizes.ratio, + centerWidth: lock.sizes.centerWidth + } + }; + } + + function serializeUtilities(): var { + return { + enabled: utilities.enabled, + maxToasts: utilities.maxToasts, + sizes: { + width: utilities.sizes.width, + toastWidth: utilities.sizes.toastWidth + }, + toasts: { + configLoaded: utilities.toasts.configLoaded, + chargingChanged: utilities.toasts.chargingChanged, + gameModeChanged: utilities.toasts.gameModeChanged, + dndChanged: utilities.toasts.dndChanged, + audioOutputChanged: utilities.toasts.audioOutputChanged, + audioInputChanged: utilities.toasts.audioInputChanged, + capsLockChanged: utilities.toasts.capsLockChanged, + numLockChanged: utilities.toasts.numLockChanged, + kbLayoutChanged: utilities.toasts.kbLayoutChanged, + vpnChanged: utilities.toasts.vpnChanged, + nowPlaying: utilities.toasts.nowPlaying + }, + vpn: { + enabled: utilities.vpn.enabled, + provider: utilities.vpn.provider + } + }; + } + + function serializeSidebar(): var { + return { + enabled: sidebar.enabled, + dragThreshold: sidebar.dragThreshold, + sizes: { + width: sidebar.sizes.width + } + }; + } + + function serializeServices(): var { + return { + weatherLocation: services.weatherLocation, + useFahrenheit: services.useFahrenheit, + useTwelveHourClock: services.useTwelveHourClock, + gpuType: services.gpuType, + visualiserBars: services.visualiserBars, + audioIncrement: services.audioIncrement, + maxVolume: services.maxVolume, + smartScheme: services.smartScheme, + defaultPlayer: services.defaultPlayer, + playerAliases: services.playerAliases + }; + } + + function serializePaths(): var { + return { + wallpaperDir: paths.wallpaperDir, + sessionGif: paths.sessionGif, + mediaGif: paths.mediaGif + }; + } + FileView { + id: fileView + path: `${Paths.config}/shell.json` watchChanges: true onFileChanged: { - timer.restart(); - reload(); + // Prevent reload loop - don't reload if we just saved + if (!saveTimer.running) { + timer.restart(); + reload(); + } } onLoaded: { try { diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 388fc88..22e5580 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -84,6 +84,9 @@ RowLayout { // Update border Config.border.rounding = root.borderRounding; Config.border.thickness = root.borderThickness; + + // Persist changes to disk + Config.save(); } Item { -- cgit v1.2.3-freya From f2e9f60344a2185d244c8eda962bb2f4e9917be0 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 21:05:50 -0500 Subject: controlcenter: max padding now 2 --- modules/controlcenter/appearance/AppearancePane.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 22e5580..37397ae 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -912,7 +912,7 @@ RowLayout { anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 5.0 } + validator: DoubleValidator { bottom: 0.1; top: 2.0 } Component.onCompleted: { text = (rootPane.paddingScale).toFixed(1); @@ -921,7 +921,7 @@ RowLayout { onTextChanged: { if (activeFocus) { const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 5.0) { + if (!isNaN(val) && val >= 0.1 && val <= 2.0) { rootPane.paddingScale = val; rootPane.saveConfig(); } @@ -929,7 +929,7 @@ RowLayout { } onEditingFinished: { const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 5.0) { + if (isNaN(val) || val < 0.1 || val > 2.0) { text = (rootPane.paddingScale).toFixed(1); } } @@ -950,7 +950,7 @@ RowLayout { implicitHeight: Appearance.padding.normal * 3 from: 0.1 - to: 5.0 + to: 2.0 value: rootPane.paddingScale onMoved: { rootPane.paddingScale = paddingSlider.value; -- cgit v1.2.3-freya From 28be7e8d33301ea5ea40b0606ff1c02711b076e1 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 21:09:10 -0500 Subject: controlcenter: slider component color fix for transparency layers --- components/controls/StyledSlider.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/controls/StyledSlider.qml b/components/controls/StyledSlider.qml index 55e8c8d..0ef229d 100644 --- a/components/controls/StyledSlider.qml +++ b/components/controls/StyledSlider.qml @@ -32,7 +32,7 @@ Slider { implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6 - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + color: Colours.palette.m3surfaceContainerHighest radius: Appearance.rounding.full topLeftRadius: root.implicitHeight / 15 bottomLeftRadius: root.implicitHeight / 15 -- cgit v1.2.3-freya From 90bda07a9031f88ff6d7df52ac39a09feba98a6d Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 21:22:14 -0500 Subject: controlcenter: animation bug fix on flickables --- components/controls/CollapsibleSection.qml | 7 + .../controlcenter/appearance/AppearancePane.qml | 546 ++++++++++----------- 2 files changed, 270 insertions(+), 283 deletions(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 3fba1c3..600b662 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -104,6 +104,13 @@ ColumnLayout { anchors.topMargin: Appearance.spacing.small anchors.bottomMargin: Appearance.spacing.small spacing: Appearance.spacing.small + opacity: root.expanded ? 1.0 : 0.0 + + Behavior on opacity { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } } } } diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 37397ae..459f4c1 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -200,84 +200,80 @@ RowLayout { sidebarFlickable.collapseAllSections(colorVariantSection); } - StyledListView { + ColumnLayout { Layout.fillWidth: true - implicitHeight: colorVariantSection.expanded ? Math.min(400, M3Variants.list.length * 60) : 0 - - model: M3Variants.list spacing: Appearance.spacing.small / 2 - clip: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } + Repeater { + model: M3Variants.list - delegate: StyledRect { - required property var modelData + delegate: StyledRect { + required property var modelData - width: parent ? parent.width : 0 + Layout.fillWidth: true - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 - border.color: Colours.palette.m3primary + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 + border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - const variant = modelData.variant; + StateLayer { + function onClicked(): void { + const variant = modelData.variant; - // Optimistic update - set immediately - Schemes.currentVariant = variant; + // Optimistic update - set immediately + Schemes.currentVariant = variant; - // Execute the command - Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); + // Execute the command + Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); - // Reload after a delay to confirm - Qt.callLater(() => { - reloadTimer.restart(); - }); + // Reload after a delay to confirm + Qt.callLater(() => { + reloadTimer.restart(); + }); + } } - } - Timer { - id: reloadTimer - interval: 300 - onTriggered: { - Schemes.reload(); + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); + } } - } - RowLayout { - id: variantRow + RowLayout { + id: variantRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - MaterialIcon { - text: modelData.icon - font.pointSize: Appearance.font.size.large - fill: modelData.variant === Schemes.currentVariant ? 1 : 0 - } + MaterialIcon { + text: modelData.icon + font.pointSize: Appearance.font.size.large + fill: modelData.variant === Schemes.currentVariant ? 1 : 0 + } - StyledText { - Layout.fillWidth: true - text: modelData.name - font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 - } + StyledText { + Layout.fillWidth: true + text: modelData.name + font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 + } - MaterialIcon { - visible: modelData.variant === Schemes.currentVariant - text: "check" - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large + MaterialIcon { + visible: modelData.variant === Schemes.currentVariant + text: "check" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.large + } } - } - implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 + } } } } @@ -290,139 +286,135 @@ RowLayout { sidebarFlickable.collapseAllSections(colorSchemeSection); } - StyledListView { + ColumnLayout { Layout.fillWidth: true - implicitHeight: colorSchemeSection.expanded ? Math.min(400, Schemes.list.length * 80) : 0 - - model: Schemes.list spacing: Appearance.spacing.small / 2 - clip: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } + Repeater { + model: Schemes.list - delegate: StyledRect { - required property var modelData + delegate: StyledRect { + required property var modelData - width: parent ? parent.width : 0 + Layout.fillWidth: true - readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` - readonly property bool isCurrent: schemeKey === Schemes.currentScheme + readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` + readonly property bool isCurrent: schemeKey === Schemes.currentScheme - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - const name = modelData.name; - const flavour = modelData.flavour; - const schemeKey = `${name} ${flavour}`; + StateLayer { + function onClicked(): void { + const name = modelData.name; + const flavour = modelData.flavour; + const schemeKey = `${name} ${flavour}`; - // Optimistic update - set immediately - Schemes.currentScheme = schemeKey; + // Optimistic update - set immediately + Schemes.currentScheme = schemeKey; - // Execute the command - Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); + // Execute the command + Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - // Reload after a delay to confirm - Qt.callLater(() => { - reloadTimer.restart(); - }); + // Reload after a delay to confirm + Qt.callLater(() => { + reloadTimer.restart(); + }); + } } - } - Timer { - id: reloadTimer - interval: 300 - onTriggered: { - Schemes.reload(); + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); + } } - } - RowLayout { - id: schemeRow - - anchors.fill: parent - anchors.margins: Appearance.padding.normal + RowLayout { + id: schemeRow - spacing: Appearance.spacing.normal - - StyledRect { - id: preview + anchors.fill: parent + anchors.margins: Appearance.padding.normal - Layout.alignment: Qt.AlignVCenter + spacing: Appearance.spacing.normal - border.width: 1 - border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) + StyledRect { + id: preview - color: `#${modelData.colours?.surface}` - radius: Appearance.rounding.full - implicitWidth: iconPlaceholder.implicitWidth - implicitHeight: iconPlaceholder.implicitWidth + Layout.alignment: Qt.AlignVCenter - MaterialIcon { - id: iconPlaceholder - visible: false - text: "circle" - font.pointSize: Appearance.font.size.large - } + border.width: 1 + border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) - Item { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right + color: `#${modelData.colours?.surface}` + radius: Appearance.rounding.full + implicitWidth: iconPlaceholder.implicitWidth + implicitHeight: iconPlaceholder.implicitWidth - implicitWidth: parent.implicitWidth / 2 - clip: true + MaterialIcon { + id: iconPlaceholder + visible: false + text: "circle" + font.pointSize: Appearance.font.size.large + } - StyledRect { + Item { anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right - implicitWidth: preview.implicitWidth - color: `#${modelData.colours?.primary}` - radius: Appearance.rounding.full + implicitWidth: parent.implicitWidth / 2 + clip: true + + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + implicitWidth: preview.implicitWidth + color: `#${modelData.colours?.primary}` + radius: Appearance.rounding.full + } } } - } - Column { - Layout.fillWidth: true - spacing: 0 + Column { + Layout.fillWidth: true + spacing: 0 - StyledText { - text: modelData.flavour ?? "" - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData.flavour ?? "" + font.pointSize: Appearance.font.size.normal + } - StyledText { - text: modelData.name ?? "" - font.pointSize: Appearance.font.size.small - color: Colours.palette.m3outline + StyledText { + text: modelData.name ?? "" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline - elide: Text.ElideRight - anchors.left: parent.left - anchors.right: parent.right + elide: Text.ElideRight + anchors.left: parent.left + anchors.right: parent.right + } } - } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 + } } } } @@ -548,68 +540,64 @@ RowLayout { font.weight: 500 } - StyledListView { + ColumnLayout { Layout.fillWidth: true - implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - - model: Qt.fontFamilies() spacing: Appearance.spacing.small / 2 - clip: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } + Repeater { + model: Qt.fontFamilies() - delegate: StyledRect { - required property string modelData + delegate: StyledRect { + required property string modelData - width: parent ? parent.width : 0 + Layout.fillWidth: true - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMaterial = modelData; - rootPane.saveConfig(); + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMaterial = modelData; + rootPane.saveConfig(); + } } - } - RowLayout { - id: fontFamilyMaterialRow + RowLayout { + id: fontFamilyMaterialRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + } } } @@ -620,68 +608,64 @@ RowLayout { font.weight: 500 } - StyledListView { + ColumnLayout { Layout.fillWidth: true - implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - - model: Qt.fontFamilies() spacing: Appearance.spacing.small / 2 - clip: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } + Repeater { + model: Qt.fontFamilies() - delegate: StyledRect { - required property string modelData + delegate: StyledRect { + required property string modelData - width: parent ? parent.width : 0 + Layout.fillWidth: true - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMono = modelData; - rootPane.saveConfig(); + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMono = modelData; + rootPane.saveConfig(); + } } - } - RowLayout { - id: fontFamilyMonoRow + RowLayout { + id: fontFamilyMonoRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + } } } @@ -692,68 +676,64 @@ RowLayout { font.weight: 500 } - StyledListView { + ColumnLayout { Layout.fillWidth: true - implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0 - - model: Qt.fontFamilies() spacing: Appearance.spacing.small / 2 - clip: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } + Repeater { + model: Qt.fontFamilies() - delegate: StyledRect { - required property string modelData + delegate: StyledRect { + required property string modelData - width: parent ? parent.width : 0 + Layout.fillWidth: true - readonly property bool isCurrent: modelData === rootPane.fontFamilySans - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + readonly property bool isCurrent: modelData === rootPane.fontFamilySans + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - rootPane.fontFamilySans = modelData; - rootPane.saveConfig(); + StateLayer { + function onClicked(): void { + rootPane.fontFamilySans = modelData; + rootPane.saveConfig(); + } } - } - RowLayout { - id: fontFamilySansRow + RowLayout { + id: fontFamilySansRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + } } } -- cgit v1.2.3-freya From 699d7ef7fcabe7e38243143a3f1aab7fa55ad8c0 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 21:24:48 -0500 Subject: controlcenter: padding and spacing scale min-max --- modules/controlcenter/appearance/AppearancePane.qml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 459f4c1..1e4ff0d 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -892,7 +892,7 @@ RowLayout { anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 2.0 } + validator: DoubleValidator { bottom: 0.5; top: 2.0 } Component.onCompleted: { text = (rootPane.paddingScale).toFixed(1); @@ -901,7 +901,7 @@ RowLayout { onTextChanged: { if (activeFocus) { const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 2.0) { + if (!isNaN(val) && val >= 0.5 && val <= 2.0) { rootPane.paddingScale = val; rootPane.saveConfig(); } @@ -909,7 +909,7 @@ RowLayout { } onEditingFinished: { const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 2.0) { + if (isNaN(val) || val < 0.5 || val > 2.0) { text = (rootPane.paddingScale).toFixed(1); } } @@ -929,7 +929,7 @@ RowLayout { Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 - from: 0.1 + from: 0.5 to: 2.0 value: rootPane.paddingScale onMoved: { @@ -1090,7 +1090,7 @@ RowLayout { anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 5.0 } + validator: DoubleValidator { bottom: 0.1; top: 2.0 } Component.onCompleted: { text = (rootPane.spacingScale).toFixed(1); @@ -1099,7 +1099,7 @@ RowLayout { onTextChanged: { if (activeFocus) { const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 5.0) { + if (!isNaN(val) && val >= 0.1 && val <= 2.0) { rootPane.spacingScale = val; rootPane.saveConfig(); } @@ -1107,7 +1107,7 @@ RowLayout { } onEditingFinished: { const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 5.0) { + if (isNaN(val) || val < 0.1 || val > 2.0) { text = (rootPane.spacingScale).toFixed(1); } } @@ -1128,7 +1128,7 @@ RowLayout { implicitHeight: Appearance.padding.normal * 3 from: 0.1 - to: 5.0 + to: 2.0 value: rootPane.spacingScale onMoved: { rootPane.spacingScale = spacingSlider.value; -- cgit v1.2.3-freya From 420a19bf31c6c97b66f6333e41fa37b9278cc6c5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 21:27:38 -0500 Subject: controlcenter: gradient rectangle around wallpapers fix --- modules/controlcenter/appearance/AppearancePane.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 1e4ff0d..38d335d 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -2008,6 +2008,8 @@ RowLayout { implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 2 + radius: Appearance.rounding.normal + gradient: Gradient { GradientStop { position: 0.0 -- cgit v1.2.3-freya From 7d839be3e5c42a06e48c5e2c7808d1579c84e07c Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 21:43:09 -0500 Subject: controlcenter: fonts performance fix (virtual listview) --- .../controlcenter/appearance/AppearancePane.qml | 550 +++++++++++---------- 1 file changed, 276 insertions(+), 274 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 38d335d..279c362 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -540,64 +540,65 @@ RowLayout { font.weight: 500 } - ColumnLayout { + StyledListView { Layout.fillWidth: true + Layout.preferredHeight: Math.min(contentHeight, 300) + + clip: true spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() - Repeater { - model: Qt.fontFamilies() + delegate: StyledRect { + required property string modelData + required property int index - delegate: StyledRect { - required property string modelData + width: ListView.view.width - Layout.fillWidth: true - - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMaterial = modelData; - rootPane.saveConfig(); - } + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMaterial = modelData; + rootPane.saveConfig(); } + } - RowLayout { - id: fontFamilyMaterialRow + RowLayout { + id: fontFamilyMaterialRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large } } - - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 } + + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 } } @@ -608,64 +609,65 @@ RowLayout { font.weight: 500 } - ColumnLayout { + StyledListView { Layout.fillWidth: true + Layout.preferredHeight: Math.min(contentHeight, 300) + + clip: true spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() - Repeater { - model: Qt.fontFamilies() + delegate: StyledRect { + required property string modelData + required property int index - delegate: StyledRect { - required property string modelData + width: ListView.view.width - Layout.fillWidth: true + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMono = modelData; - rootPane.saveConfig(); - } + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMono = modelData; + rootPane.saveConfig(); } + } - RowLayout { - id: fontFamilyMonoRow + RowLayout { + id: fontFamilyMonoRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large } } - - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 } + + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 } } @@ -676,64 +678,65 @@ RowLayout { font.weight: 500 } - ColumnLayout { + StyledListView { Layout.fillWidth: true + Layout.preferredHeight: Math.min(contentHeight, 300) + + clip: true spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() - Repeater { - model: Qt.fontFamilies() + delegate: StyledRect { + required property string modelData + required property int index - delegate: StyledRect { - required property string modelData + width: ListView.view.width - Layout.fillWidth: true + readonly property bool isCurrent: modelData === rootPane.fontFamilySans + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - readonly property bool isCurrent: modelData === rootPane.fontFamilySans - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - rootPane.fontFamilySans = modelData; - rootPane.saveConfig(); - } + StateLayer { + function onClicked(): void { + rootPane.fontFamilySans = modelData; + rootPane.saveConfig(); } + } - RowLayout { - id: fontFamilySansRow + RowLayout { + id: fontFamilySansRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large } } - - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 } + + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 } } @@ -1874,210 +1877,209 @@ RowLayout { color: Colours.palette.m3onSurfaceVariant } - GridLayout { + GridView { + id: wallpaperGrid Layout.fillWidth: true Layout.topMargin: Appearance.spacing.large + Layout.preferredHeight: Math.min(600, Math.ceil(count / Math.floor(width / cellWidth)) * cellHeight) Layout.alignment: Qt.AlignHCenter - columns: Math.max(2, Math.floor(parent.width / 200)) - rowSpacing: Appearance.spacing.normal - columnSpacing: Appearance.spacing.normal + cellWidth: 200 + Appearance.spacing.normal + cellHeight: 140 + Appearance.spacing.normal + + model: Wallpapers.list + clip: true - // Center the grid content - Layout.maximumWidth: { - const cols = columns; - const itemWidth = 200; - const spacing = columnSpacing; - return cols * itemWidth + (cols - 1) * spacing; + // Enable caching for better performance + cacheBuffer: cellHeight * 2 + + StyledScrollBar.vertical: StyledScrollBar { + flickable: wallpaperGrid } - Repeater { - model: Wallpapers.list + delegate: Item { + required property var modelData - delegate: Item { - required property var modelData + width: 200 + height: 140 - Layout.preferredWidth: 200 - Layout.preferredHeight: 140 - Layout.minimumWidth: 200 - Layout.minimumHeight: 140 - Layout.maximumWidth: 200 - Layout.maximumHeight: 140 + // Center in cell + x: (wallpaperGrid.cellWidth - width) / 2 + y: (wallpaperGrid.cellHeight - height) / 2 - readonly property bool isCurrent: modelData.path === Wallpapers.actualCurrent + readonly property bool isCurrent: modelData.path === Wallpapers.actualCurrent - StateLayer { - radius: Appearance.rounding.normal + StateLayer { + radius: Appearance.rounding.normal - function onClicked(): void { - Wallpapers.setWallpaper(modelData.path); - } + function onClicked(): void { + Wallpapers.setWallpaper(modelData.path); } + } - StyledClippingRect { - id: image + StyledClippingRect { + id: image - anchors.fill: parent - color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal + anchors.fill: parent + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal - CachingImage { - id: cachingImage + CachingImage { + id: cachingImage - path: modelData.path - anchors.fill: parent - cache: true - visible: opacity > 0 + path: modelData.path + anchors.fill: parent + cache: true + visible: opacity > 0 - // Show when ready - opacity: status === Image.Ready ? 1 : 0 + // Show when ready + opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutQuad - } + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutQuad } } + } - // Fallback image for when caching fails - Image { - id: fallbackImage + // Fallback image for when caching fails + 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 + anchors.fill: parent + source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : "" + asynchronous: true + fillMode: Image.PreserveAspectCrop + cache: true + visible: opacity > 0 - opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 + opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutQuad - } + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutQuad } } + } - // Timer to trigger fallback only if caching hasn't loaded - Timer { - id: fallbackTimer + // Timer to trigger fallback only if caching hasn't loaded + Timer { + id: fallbackTimer - property bool triggered: false - interval: 800 - running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null - onTriggered: triggered = true - } + property bool triggered: false + interval: 800 + running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null + onTriggered: triggered = true } + } - // Border overlay that doesn't affect image size - Rectangle { - anchors.fill: parent - color: "transparent" - radius: Appearance.rounding.normal - border.width: isCurrent ? 2 : 0 - border.color: Colours.palette.m3primary + // Border overlay that doesn't affect image size + Rectangle { + anchors.fill: parent + color: "transparent" + radius: Appearance.rounding.normal + border.width: isCurrent ? 2 : 0 + border.color: Colours.palette.m3primary - Behavior on border.width { - NumberAnimation { - duration: 150 - easing.type: Easing.OutQuad - } + 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 + 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 - } + visible: isCurrent + text: "check_circle" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.large + } - // Gradient overlay for filename with rounded bottom corners - Rectangle { - id: filenameOverlay + // Gradient overlay for filename with rounded bottom corners + Rectangle { + id: filenameOverlay - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom - implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Appearance.rounding.normal - gradient: Gradient { - GradientStop { - position: 0.0 - color: Qt.rgba(0, 0, 0, 0) - } - GradientStop { - position: 0.3 - color: Qt.rgba(0, 0, 0, 0.3) - } - GradientStop { - position: 0.7 - color: Qt.rgba(0, 0, 0, 0.75) - } - GradientStop { - position: 1.0 - color: Qt.rgba(0, 0, 0, 0.85) - } + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.rgba(0, 0, 0, 0) } - } - - opacity: 0 - - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic + GradientStop { + position: 0.3 + color: Qt.rgba(0, 0, 0, 0.3) + } + GradientStop { + position: 0.7 + color: Qt.rgba(0, 0, 0, 0.75) + } + GradientStop { + position: 1.0 + color: Qt.rgba(0, 0, 0, 0.85) } } + } + + opacity: 0 - Component.onCompleted: { - opacity = 1; + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic } } - StyledText { - id: filenameText - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.leftMargin: Appearance.padding.normal - anchors.rightMargin: Appearance.padding.normal - anchors.bottomMargin: Appearance.padding.normal + Component.onCompleted: { + opacity = 1; + } + } - readonly property string fileName: { - const path = modelData.relativePath || ""; - const parts = path.split("/"); - return parts.length > 0 ? parts[parts.length - 1] : path; - } + StyledText { + id: filenameText + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + 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 : "#FFFFFF" - elide: Text.ElideMiddle - maximumLineCount: 1 + text: fileName + font.pointSize: Appearance.font.size.smaller + font.weight: 500 + color: isCurrent ? Colours.palette.m3primary : "#FFFFFF" + elide: Text.ElideMiddle + maximumLineCount: 1 - opacity: 0 + opacity: 0 - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic - } + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic } + } - Component.onCompleted: { - opacity = 1; - } + Component.onCompleted: { + opacity = 1; } } } -- cgit v1.2.3-freya From 483a6fe8e4297d56e9f4dc5cb33c046520edc538 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 21:54:33 -0500 Subject: controlcenter: collapsible section sub heading animation correction --- components/controls/CollapsibleSection.qml | 39 +++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 600b662..e1cd35f 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -68,16 +68,37 @@ ColumnLayout { } } - StyledText { - visible: root.expanded && root.description !== "" + Item { + visible: root.description !== "" Layout.fillWidth: true - Layout.leftMargin: Appearance.padding.normal - Layout.rightMargin: Appearance.padding.normal - Layout.topMargin: Appearance.spacing.smaller - Layout.bottomMargin: Appearance.spacing.small - text: root.description - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + Layout.preferredHeight: root.expanded ? descriptionText.implicitHeight + Appearance.spacing.smaller + Appearance.spacing.small : 0 + clip: true + + Behavior on Layout.preferredHeight { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + StyledText { + id: descriptionText + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + anchors.topMargin: Appearance.spacing.smaller + text: root.description + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + opacity: root.expanded ? 1.0 : 0.0 + + Behavior on opacity { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } } default property alias content: contentColumn.data -- cgit v1.2.3-freya From c3d9fb8dc0c0aeb9b1a0f47d9e2e770af42daa1b Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 22:03:27 -0500 Subject: controlcenter: wallpaper gradient/outline 1px fix --- modules/controlcenter/appearance/AppearancePane.qml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 279c362..b94a1e2 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -1923,6 +1923,7 @@ RowLayout { anchors.fill: parent color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.normal + antialiasing: true CachingImage { id: cachingImage @@ -1931,6 +1932,7 @@ RowLayout { anchors.fill: parent cache: true visible: opacity > 0 + antialiasing: true // Show when ready opacity: status === Image.Ready ? 1 : 0 @@ -1953,6 +1955,7 @@ RowLayout { fillMode: Image.PreserveAspectCrop cache: true visible: opacity > 0 + antialiasing: true opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 @@ -1982,6 +1985,7 @@ RowLayout { radius: Appearance.rounding.normal border.width: isCurrent ? 2 : 0 border.color: Colours.palette.m3primary + antialiasing: true Behavior on border.width { NumberAnimation { @@ -2019,16 +2023,16 @@ RowLayout { color: Qt.rgba(0, 0, 0, 0) } GradientStop { - position: 0.3 - color: Qt.rgba(0, 0, 0, 0.3) + position: 0.4 + color: Qt.rgba(0, 0, 0, 0.2) } GradientStop { - position: 0.7 - color: Qt.rgba(0, 0, 0, 0.75) + position: 0.8 + color: Qt.rgba(0, 0, 0, 0.5) } GradientStop { position: 1.0 - color: Qt.rgba(0, 0, 0, 0.85) + color: Qt.rgba(0, 0, 0, 0.6) } } } -- cgit v1.2.3-freya From ce3902d8f34922401d7bec542c6116091aa2cf58 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 23:41:31 -0500 Subject: controlcenter: wallpaper selector --- .../controlcenter/appearance/AppearancePane.qml | 130 +++++++++++++++------ 1 file changed, 94 insertions(+), 36 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index b94a1e2..11eb2b0 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -1812,10 +1812,7 @@ RowLayout { Loader { id: rightAppearanceLoader anchors.fill: parent - anchors.topMargin: Appearance.padding.large * 2 - anchors.bottomMargin: Appearance.padding.large * 2 - anchors.leftMargin: 0 - anchors.rightMargin: 0 + anchors.margins: Appearance.padding.large * 2 asynchronous: true sourceComponent: appearanceRightContentComponent property var rootPane: root @@ -1844,12 +1841,12 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right - anchors.leftMargin: Appearance.padding.large * 2 - anchors.rightMargin: Appearance.padding.large * 2 + anchors.top: parent.top spacing: Appearance.spacing.normal MaterialIcon { - Layout.alignment: Qt.AlignHCenter + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.topMargin: 0 text: "palette" font.pointSize: Appearance.font.size.extraLarge * 3 font.bold: true @@ -1877,39 +1874,53 @@ RowLayout { color: Colours.palette.m3onSurfaceVariant } - GridView { - id: wallpaperGrid + Item { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.large - Layout.preferredHeight: Math.min(600, Math.ceil(count / Math.floor(width / cellWidth)) * cellHeight) - Layout.alignment: Qt.AlignHCenter - - cellWidth: 200 + Appearance.spacing.normal - cellHeight: 140 + Appearance.spacing.normal + Layout.preferredHeight: wallpaperGrid.Layout.preferredHeight + + GridView { + id: wallpaperGrid + anchors.fill: parent + + readonly property int minCellWidth: 200 + Appearance.spacing.normal + readonly property int columnsCount: Math.max(1, Math.floor(parent.width / minCellWidth)) + + Layout.preferredHeight: Math.ceil(count / columnsCount) * cellHeight + height: Layout.preferredHeight + + // Distribute width evenly across columns + cellWidth: width / columnsCount + cellHeight: 140 + Appearance.spacing.normal + + leftMargin: 0 + rightMargin: 0 + topMargin: 0 + bottomMargin: 0 - model: Wallpapers.list - clip: true + model: Wallpapers.list + clip: true + + // Disable GridView's own scrolling - let parent handle it + interactive: false // Enable caching for better performance cacheBuffer: cellHeight * 2 - StyledScrollBar.vertical: StyledScrollBar { - flickable: wallpaperGrid - } - delegate: Item { required property var modelData - width: 200 - height: 140 - - // Center in cell - x: (wallpaperGrid.cellWidth - width) / 2 - y: (wallpaperGrid.cellHeight - height) / 2 + width: wallpaperGrid.cellWidth + height: wallpaperGrid.cellHeight readonly property bool isCurrent: modelData.path === Wallpapers.actualCurrent StateLayer { + anchors.fill: parent + anchors.leftMargin: Appearance.spacing.normal / 2 + anchors.rightMargin: Appearance.spacing.normal / 2 + anchors.topMargin: Appearance.spacing.normal / 2 + anchors.bottomMargin: Appearance.spacing.normal / 2 radius: Appearance.rounding.normal function onClicked(): void { @@ -1921,6 +1932,10 @@ RowLayout { id: image anchors.fill: parent + anchors.leftMargin: Appearance.spacing.normal / 2 + anchors.rightMargin: Appearance.spacing.normal / 2 + anchors.topMargin: Appearance.spacing.normal / 2 + anchors.bottomMargin: Appearance.spacing.normal / 2 color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.normal antialiasing: true @@ -1930,6 +1945,7 @@ RowLayout { path: modelData.path anchors.fill: parent + fillMode: Image.PreserveAspectCrop cache: true visible: opacity > 0 antialiasing: true @@ -1981,6 +1997,10 @@ RowLayout { // Border overlay that doesn't affect image size Rectangle { anchors.fill: parent + anchors.leftMargin: Appearance.spacing.normal / 2 + anchors.rightMargin: Appearance.spacing.normal / 2 + anchors.topMargin: Appearance.spacing.normal / 2 + anchors.bottomMargin: Appearance.spacing.normal / 2 color: "transparent" radius: Appearance.rounding.normal border.width: isCurrent ? 2 : 0 @@ -2012,27 +2032,63 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom + anchors.leftMargin: isCurrent ? 2 : 0 + anchors.rightMargin: isCurrent ? 2 : 0 + anchors.bottomMargin: isCurrent ? 2 : 0 - implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5 - radius: Appearance.rounding.normal + // Only round bottom corners + topLeftRadius: 0 + topRightRadius: 0 + bottomLeftRadius: Appearance.rounding.normal + bottomRightRadius: Appearance.rounding.normal + + Behavior on anchors.leftMargin { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + + Behavior on anchors.rightMargin { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + + Behavior on anchors.bottomMargin { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } gradient: Gradient { GradientStop { position: 0.0 - color: Qt.rgba(0, 0, 0, 0) + color: Qt.rgba(Colours.palette.m3surfaceContainer.r, + Colours.palette.m3surfaceContainer.g, + Colours.palette.m3surfaceContainer.b, 0) } GradientStop { - position: 0.4 - color: Qt.rgba(0, 0, 0, 0.2) + position: 0.3 + color: Qt.rgba(Colours.palette.m3surfaceContainer.r, + Colours.palette.m3surfaceContainer.g, + Colours.palette.m3surfaceContainer.b, 0.7) } GradientStop { - position: 0.8 - color: Qt.rgba(0, 0, 0, 0.5) + 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(0, 0, 0, 0.6) + color: Qt.rgba(Colours.palette.m3surfaceContainer.r, + Colours.palette.m3surfaceContainer.g, + Colours.palette.m3surfaceContainer.b, 0.95) } } } @@ -2069,9 +2125,10 @@ RowLayout { text: fileName font.pointSize: Appearance.font.size.smaller font.weight: 500 - color: isCurrent ? Colours.palette.m3primary : "#FFFFFF" + color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface elide: Text.ElideMiddle maximumLineCount: 1 + horizontalAlignment: Text.AlignHCenter opacity: 0 @@ -2088,8 +2145,9 @@ RowLayout { } } } - } + } } } } + } } -- cgit v1.2.3-freya From 63d680404f82a80a820a0273409bd52ee3cf2b00 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 08:56:38 -0500 Subject: controlcenter: font slider min-max --- modules/controlcenter/appearance/AppearancePane.qml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 11eb2b0..e91448e 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -788,7 +788,7 @@ RowLayout { anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 5.0 } + validator: DoubleValidator { bottom: 0.7; top: 1.5 } Component.onCompleted: { text = (rootPane.fontSizeScale).toFixed(1); @@ -797,7 +797,7 @@ RowLayout { onTextChanged: { if (activeFocus) { const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 5.0) { + if (!isNaN(val) && val >= 0.7 && val <= 1.5) { rootPane.fontSizeScale = val; rootPane.saveConfig(); } @@ -805,7 +805,7 @@ RowLayout { } onEditingFinished: { const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 5.0) { + if (isNaN(val) || val < 0.7 || val > 1.5) { text = (rootPane.fontSizeScale).toFixed(1); } } @@ -825,8 +825,8 @@ RowLayout { Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 - from: 0.1 - to: 5.0 + from: 0.7 + to: 1.5 value: rootPane.fontSizeScale onMoved: { rootPane.fontSizeScale = fontSizeSlider.value; -- cgit v1.2.3-freya From 0d57edcade29fa1da2eef510dc83d5bb26528d57 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 09:03:16 -0500 Subject: config: 0ms bug on save corrected --- config/Config.qml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/config/Config.qml b/config/Config.qml index 3756043..b875eef 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -30,8 +30,12 @@ Singleton { // Public save function - call this to persist config changes function save(): void { saveTimer.restart(); + recentlySaved = true; + recentSaveCooldown.restart(); } + property bool recentlySaved: false + ElapsedTimer { id: timer } @@ -41,6 +45,7 @@ Singleton { interval: 500 onTriggered: { + timer.restart(); try { // Parse current config to preserve structure and comments if possible let config = {}; @@ -62,6 +67,15 @@ Singleton { } } + Timer { + id: recentSaveCooldown + + interval: 2000 + onTriggered: { + recentlySaved = false; + } + } + // Helper function to serialize the config object function serializeConfig(): var { return { @@ -409,16 +423,24 @@ Singleton { watchChanges: true onFileChanged: { // Prevent reload loop - don't reload if we just saved - if (!saveTimer.running) { + if (!recentlySaved) { timer.restart(); reload(); + } else { + // Self-initiated save - reload without toast + reload(); } } onLoaded: { try { JSON.parse(text()); - if (adapter.utilities.toasts.configLoaded) - Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(timer.elapsedMs()), "rule_settings"); + const elapsed = timer.elapsedMs(); + // Only show toast for external changes (not our own saves) and when elapsed time is meaningful + if (adapter.utilities.toasts.configLoaded && !recentlySaved && elapsed > 0) { + Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings"); + } else if (adapter.utilities.toasts.configLoaded && recentlySaved && elapsed > 0) { + Toaster.toast(qsTr("Config saved"), qsTr("Config reloaded in %1ms").arg(elapsed), "rule_settings"); + } } catch (e) { Toaster.toast(qsTr("Failed to load config"), e.message, "settings_alert", Toast.Error); } -- cgit v1.2.3-freya From c9edb4d0645ebbfb513bb56e731a46bd83b591c6 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 09:07:16 -0500 Subject: controlcenter: animation subtext in collapsiblesection correction --- components/controls/CollapsibleSection.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index e1cd35f..2df169a 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -69,9 +69,8 @@ ColumnLayout { } Item { - visible: root.description !== "" Layout.fillWidth: true - Layout.preferredHeight: root.expanded ? descriptionText.implicitHeight + Appearance.spacing.smaller + Appearance.spacing.small : 0 + Layout.preferredHeight: (root.expanded && root.description !== "") ? descriptionText.implicitHeight + Appearance.spacing.smaller + Appearance.spacing.small : 0 clip: true Behavior on Layout.preferredHeight { @@ -91,7 +90,7 @@ ColumnLayout { text: root.description color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small - opacity: root.expanded ? 1.0 : 0.0 + opacity: (root.expanded && root.description !== "") ? 1.0 : 0.0 Behavior on opacity { Anim { -- cgit v1.2.3-freya From 95615e64b77ea19f099ab0339f9c074362705ccf Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 09:25:56 -0500 Subject: controlcenter: wallpaper selector now spacing scale aware --- modules/controlcenter/appearance/AppearancePane.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index e91448e..45f622f 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -2112,8 +2112,8 @@ RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.leftMargin: Appearance.padding.normal - anchors.rightMargin: Appearance.padding.normal + 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: { -- cgit v1.2.3-freya From d3124c34ace89443558b629c2178969001038d7d Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 09:29:52 -0500 Subject: controlcenter: slider input focus on pane change or click outside of text area --- modules/controlcenter/Panes.qml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 77b2fd2..5238624 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -20,6 +20,27 @@ ClippingRectangle { color: "transparent" clip: true + focus: false + activeFocusOnTab: false + + // Clear focus when clicking anywhere in the panes area + MouseArea { + anchors.fill: parent + z: -1 + onPressed: function(mouse) { + root.focus = true; + mouse.accepted = false; + } + } + + // Clear focus when switching panes + Connections { + target: root.session + + function onActiveIndexChanged(): void { + root.focus = true; + } + } ColumnLayout { id: layout -- cgit v1.2.3-freya From 21e715468e9800a73c731921c27177731033d2ec Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 09:45:05 -0500 Subject: controlcenter: changed to popout vs floating to match the other settings buttons (fixed issues with resizing) --- modules/bar/components/Settings.qml | 4 +- modules/bar/components/SettingsIcon.qml | 4 +- modules/controlcenter/NavRail.qml | 119 +------------------------------- modules/drawers/Panels.qml | 1 + modules/utilities/Content.qml | 2 + modules/utilities/Wrapper.qml | 2 + modules/utilities/cards/Toggles.qml | 5 +- 7 files changed, 14 insertions(+), 123 deletions(-) diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml index e92fc46..0eceea7 100644 --- a/modules/bar/components/Settings.qml +++ b/modules/bar/components/Settings.qml @@ -21,7 +21,9 @@ Item { radius: Appearance.rounding.full function onClicked(): void { - WindowFactory.create(); + WindowFactory.create(null, { + active: "appearance" + }); } } diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml index e92fc46..0eceea7 100644 --- a/modules/bar/components/SettingsIcon.qml +++ b/modules/bar/components/SettingsIcon.qml @@ -21,7 +21,9 @@ Item { radius: Appearance.rounding.full function onClicked(): void { - WindowFactory.create(); + WindowFactory.create(null, { + active: "appearance" + }); } } diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 234f447..8432f17 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -30,129 +30,12 @@ Item { PropertyChanges { layout.spacing: Appearance.spacing.small - menuIcon.opacity: 0 - menuIconExpanded.opacity: 1 - menuIcon.rotation: 180 - menuIconExpanded.rotation: 0 } } transitions: Transition { Anim { - properties: "spacing,opacity,rotation" - } - } - - Item { - id: menuBtn - - Layout.topMargin: Appearance.spacing.large - implicitWidth: menuIcon.implicitWidth + menuIcon.anchors.leftMargin * 2 - implicitHeight: menuIcon.implicitHeight + Appearance.padding.normal * 2 - - StateLayer { - radius: Appearance.rounding.small - - function onClicked(): void { - root.session.navExpanded = !root.session.navExpanded; - } - } - - MaterialIcon { - id: menuIcon - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.large - - text: "menu" - font.pointSize: Appearance.font.size.large - } - - MaterialIcon { - id: menuIconExpanded - - anchors.fill: menuIcon - text: "menu_open" - font.pointSize: menuIcon.font.pointSize - opacity: 0 - rotation: -180 - } - } - - Loader { - asynchronous: true - active: !root.session.floating - visible: active - - sourceComponent: StyledRect { - readonly property int nonAnimWidth: normalWinIcon.implicitWidth + (root.session.navExpanded ? normalWinLabel.anchors.leftMargin + normalWinLabel.implicitWidth : 0) + normalWinIcon.anchors.leftMargin * 2 - - implicitWidth: nonAnimWidth - implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Appearance.padding.normal * 2 : nonAnimWidth - - color: Colours.palette.m3primaryContainer - radius: Appearance.rounding.small - - StateLayer { - id: normalWinState - - color: Colours.palette.m3onPrimaryContainer - - function onClicked(): void { - root.session.root.close(); - WindowFactory.create(null, { - screen: root.screen, - active: root.session.active, - navExpanded: root.session.navExpanded - }); - } - } - - MaterialIcon { - id: normalWinIcon - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.large - - text: "select_window" - color: Colours.palette.m3onPrimaryContainer - font.pointSize: Appearance.font.size.large - fill: 1 - } - - StyledText { - id: normalWinLabel - - anchors.left: normalWinIcon.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.spacing.normal - - text: qsTr("Float window") - color: Colours.palette.m3onPrimaryContainer - opacity: root.session.navExpanded ? 1 : 0 - - Behavior on opacity { - Anim { - duration: Appearance.anim.durations.small - } - } - } - - Behavior on implicitWidth { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } + properties: "spacing" } } diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 4ce1182..7705732 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -109,6 +109,7 @@ Item { visibilities: root.visibilities sidebar: sidebar + popouts: popouts anchors.bottom: parent.bottom anchors.right: parent.right diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml index d5be824..902656d 100644 --- a/modules/utilities/Content.qml +++ b/modules/utilities/Content.qml @@ -8,6 +8,7 @@ Item { required property var props required property var visibilities + required property Item popouts implicitWidth: layout.implicitWidth implicitHeight: layout.implicitHeight @@ -28,6 +29,7 @@ Item { Toggles { visibilities: root.visibilities + popouts: root.popouts } } diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index dd784bc..77178e3 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -10,6 +10,7 @@ Item { required property var visibilities required property Item sidebar + required property Item popouts readonly property PersistentProperties props: PersistentProperties { property bool recordingListExpanded: false @@ -89,6 +90,7 @@ Item { implicitWidth: root.implicitWidth - Appearance.padding.large * 2 props: root.props visibilities: root.visibilities + popouts: root.popouts } } } diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index d3ad51e..71f4d1d 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -12,6 +12,7 @@ StyledRect { id: root required property var visibilities + required property Item popouts Layout.fillWidth: true implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 @@ -67,9 +68,7 @@ StyledRect { toggle: false onClicked: { root.visibilities.utilities = false; - WindowFactory.create(null, { - screen: QsWindow.window?.screen ?? null - }); + root.popouts.detach("appearance"); } } -- cgit v1.2.3-freya From d184f8d673a4b3c92a7ea396082b031d283bd4c5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 10:56:57 -0500 Subject: controlcenter: re-corrected bind loop --- modules/dashboard/Content.qml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 8a9ed5f..707bee3 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -87,6 +87,7 @@ Item { id: row Pane { + index: 0 sourceComponent: Dash { visibilities: root.visibilities state: root.state @@ -95,12 +96,14 @@ Item { } Pane { + index: 1 sourceComponent: Media { visibilities: root.visibilities } } Pane { + index: 2 sourceComponent: Performance {} } } @@ -126,12 +129,14 @@ Item { } component Pane: Loader { + required property int index + Layout.alignment: Qt.AlignTop Component.onCompleted: active = Qt.binding(() => { - const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); - const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); - return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); + const current = view.currentIndex; + // Activate current pane and adjacent panes for smooth scrolling + return Math.abs(index - current) <= 1; }) } } -- cgit v1.2.3-freya From 1ce742283836e8133054bdddfbd0852b62ac6c7b Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 12:36:50 -0500 Subject: controlcenter: 1px issue and radii uniform issues fixed --- .../controlcenter/appearance/AppearancePane.qml | 137 +++++++++------------ 1 file changed, 58 insertions(+), 79 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 45f622f..afc792b 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -1899,7 +1899,6 @@ RowLayout { bottomMargin: 0 model: Wallpapers.list - clip: true // Disable GridView's own scrolling - let parent handle it interactive: false @@ -1914,14 +1913,16 @@ RowLayout { 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 StateLayer { anchors.fill: parent - anchors.leftMargin: Appearance.spacing.normal / 2 - anchors.rightMargin: Appearance.spacing.normal / 2 - anchors.topMargin: Appearance.spacing.normal / 2 - anchors.bottomMargin: Appearance.spacing.normal / 2 - radius: Appearance.rounding.normal + anchors.leftMargin: itemMargin + anchors.rightMargin: itemMargin + anchors.topMargin: itemMargin + anchors.bottomMargin: itemMargin + radius: itemRadius function onClicked(): void { Wallpapers.setWallpaper(modelData.path); @@ -1932,13 +1933,15 @@ RowLayout { id: image anchors.fill: parent - anchors.leftMargin: Appearance.spacing.normal / 2 - anchors.rightMargin: Appearance.spacing.normal / 2 - anchors.topMargin: Appearance.spacing.normal / 2 - anchors.bottomMargin: Appearance.spacing.normal / 2 + anchors.leftMargin: itemMargin + anchors.rightMargin: itemMargin + anchors.topMargin: itemMargin + anchors.bottomMargin: itemMargin color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal + radius: itemRadius antialiasing: true + layer.enabled: true + layer.smooth: true CachingImage { id: cachingImage @@ -1949,6 +1952,7 @@ RowLayout { cache: true visible: opacity > 0 antialiasing: true + smooth: true // Show when ready opacity: status === Image.Ready ? 1 : 0 @@ -1972,6 +1976,7 @@ RowLayout { cache: true visible: opacity > 0 antialiasing: true + smooth: true opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 @@ -1992,79 +1997,20 @@ RowLayout { running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null onTriggered: triggered = true } - } - // Border overlay that doesn't affect image size - Rectangle { - anchors.fill: parent - anchors.leftMargin: Appearance.spacing.normal / 2 - anchors.rightMargin: Appearance.spacing.normal / 2 - anchors.topMargin: Appearance.spacing.normal / 2 - anchors.bottomMargin: Appearance.spacing.normal / 2 - color: "transparent" - radius: Appearance.rounding.normal - border.width: isCurrent ? 2 : 0 - border.color: Colours.palette.m3primary - antialiasing: 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 - } - - // Gradient overlay for filename with rounded bottom corners + // Gradient overlay for filename - positioned inside image container for perfect alignment Rectangle { id: filenameOverlay anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.leftMargin: isCurrent ? 2 : 0 - anchors.rightMargin: isCurrent ? 2 : 0 - anchors.bottomMargin: isCurrent ? 2 : 0 implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5 - // Only round bottom corners - topLeftRadius: 0 - topRightRadius: 0 - bottomLeftRadius: Appearance.rounding.normal - bottomRightRadius: Appearance.rounding.normal - - Behavior on anchors.leftMargin { - NumberAnimation { - duration: 150 - easing.type: Easing.OutQuad - } - } - - Behavior on anchors.rightMargin { - NumberAnimation { - duration: 150 - easing.type: Easing.OutQuad - } - } + // No rounded corners - clipped by parent's rounded corners + radius: 0 - Behavior on anchors.bottomMargin { - NumberAnimation { - duration: 150 - easing.type: Easing.OutQuad - } - } - gradient: Gradient { GradientStop { position: 0.0 @@ -2091,19 +2037,52 @@ RowLayout { Colours.palette.m3surfaceContainer.b, 0.95) } } + + opacity: 0 + + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic + } + } + + Component.onCompleted: { + opacity = 1; + } } + } - opacity: 0 + // Border overlay that doesn't affect image size + 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 opacity { + Behavior on border.width { NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic + duration: 150 + easing.type: Easing.OutQuad } } - Component.onCompleted: { - opacity = 1; + 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 } } -- cgit v1.2.3-freya From b05650b45171cb50797122d9afaca3e084a0bb4a Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 12:59:49 -0500 Subject: controlcenter: lock icon + security type --- modules/controlcenter/network/NetworkingPane.qml | 29 ++++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index c3621bb..1dd798d 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -371,11 +371,30 @@ Item { text: (modelData && modelData.ssid) ? modelData.ssid : qsTr("Unknown") } - StyledText { - text: (modelData && modelData.active) ? qsTr("Connected") : ((modelData && modelData.isSecure) ? qsTr("Secured") : qsTr("Open")) - color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: (modelData && modelData.active) ? 500 : 400 + RowLayout { + spacing: Appearance.spacing.smaller + + MaterialIcon { + visible: (modelData && modelData.isSecure) + text: "lock" + font.pointSize: Appearance.font.size.small + color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline + } + + StyledText { + text: { + if (!modelData) return qsTr("Open"); + if (modelData.active) return qsTr("Connected"); + if (modelData.isSecure && modelData.security && modelData.security.length > 0) { + return modelData.security; + } + if (modelData.isSecure) return qsTr("Secured"); + return qsTr("Open"); + } + color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: (modelData && modelData.active) ? 500 : 400 + } } StyledRect { -- cgit v1.2.3-freya From 76029cd8a08086102adcb57585c32fc4ed5e0db0 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 13:04:03 -0500 Subject: controlcenter: font subsections collapsiblesections --- .../controlcenter/appearance/AppearancePane.qml | 297 ++++++++++----------- 1 file changed, 147 insertions(+), 150 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index afc792b..13ebf55 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -533,210 +533,207 @@ RowLayout { sidebarFlickable.collapseAllSections(fontsSection); } - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Material font family") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + CollapsibleSection { + id: materialFontSection + title: qsTr("Material font family") + expanded: true - StyledListView { - Layout.fillWidth: true - Layout.preferredHeight: Math.min(contentHeight, 300) - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() + StyledListView { + Layout.fillWidth: true + Layout.preferredHeight: Math.min(contentHeight, 300) + + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() - delegate: StyledRect { - required property string modelData - required property int index + delegate: StyledRect { + required property string modelData + required property int index - width: ListView.view.width + width: ListView.view.width - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMaterial = modelData; - rootPane.saveConfig(); + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMaterial = modelData; + rootPane.saveConfig(); + } } - } - RowLayout { - id: fontFamilyMaterialRow + RowLayout { + id: fontFamilyMaterialRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + } } } - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Monospace font family") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + CollapsibleSection { + id: monoFontSection + title: qsTr("Monospace font family") + expanded: false - StyledListView { - Layout.fillWidth: true - Layout.preferredHeight: Math.min(contentHeight, 300) - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() + StyledListView { + Layout.fillWidth: true + Layout.preferredHeight: Math.min(contentHeight, 300) + + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() - delegate: StyledRect { - required property string modelData - required property int index + delegate: StyledRect { + required property string modelData + required property int index - width: ListView.view.width + width: ListView.view.width - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMono = modelData; - rootPane.saveConfig(); + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMono = modelData; + rootPane.saveConfig(); + } } - } - RowLayout { - id: fontFamilyMonoRow + RowLayout { + id: fontFamilyMonoRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + } } } - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Sans-serif font family") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + CollapsibleSection { + id: sansFontSection + title: qsTr("Sans-serif font family") + expanded: false - StyledListView { - Layout.fillWidth: true - Layout.preferredHeight: Math.min(contentHeight, 300) - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() + StyledListView { + Layout.fillWidth: true + Layout.preferredHeight: Math.min(contentHeight, 300) + + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() - delegate: StyledRect { - required property string modelData - required property int index + delegate: StyledRect { + required property string modelData + required property int index - width: ListView.view.width + width: ListView.view.width - readonly property bool isCurrent: modelData === rootPane.fontFamilySans - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + readonly property bool isCurrent: modelData === rootPane.fontFamilySans + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StateLayer { - function onClicked(): void { - rootPane.fontFamilySans = modelData; - rootPane.saveConfig(); + StateLayer { + function onClicked(): void { + rootPane.fontFamilySans = modelData; + rootPane.saveConfig(); + } } - } - RowLayout { - id: fontFamilySansRow + RowLayout { + id: fontFamilySansRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + } } } -- cgit v1.2.3-freya From 59c906d6508ccbc6f4731f222bb56a9a5e9f5345 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 13:09:09 -0500 Subject: controlcenter: appearance pane async loader --- modules/bar/components/Settings.qml | 2 +- modules/bar/components/SettingsIcon.qml | 2 +- .../controlcenter/appearance/AppearancePane.qml | 303 +++++++++++---------- modules/utilities/cards/Toggles.qml | 2 +- 4 files changed, 165 insertions(+), 144 deletions(-) diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml index 0eceea7..7cd18be 100644 --- a/modules/bar/components/Settings.qml +++ b/modules/bar/components/Settings.qml @@ -22,7 +22,7 @@ Item { function onClicked(): void { WindowFactory.create(null, { - active: "appearance" + active: "network" }); } } diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml index 0eceea7..7cd18be 100644 --- a/modules/bar/components/SettingsIcon.qml +++ b/modules/bar/components/SettingsIcon.qml @@ -22,7 +22,7 @@ Item { function onClicked(): void { WindowFactory.create(null, { - active: "appearance" + active: "network" }); } } diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 13ebf55..09cb04f 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -538,65 +538,72 @@ RowLayout { title: qsTr("Material font family") expanded: true - StyledListView { + Loader { Layout.fillWidth: true - Layout.preferredHeight: Math.min(contentHeight, 300) - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() - - delegate: StyledRect { - required property string modelData - required property int index - - width: ListView.view.width - - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMaterial = modelData; - rootPane.saveConfig(); + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: materialFontSection.expanded + + sourceComponent: StyledListView { + id: materialFontList + property alias contentHeight: materialFontList.contentHeight + + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() + + delegate: StyledRect { + required property string modelData + required property int index + + width: ListView.view.width + + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMaterial = modelData; + rootPane.saveConfig(); + } } - } - RowLayout { - id: fontFamilyMaterialRow + RowLayout { + id: fontFamilyMaterialRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + } } } } @@ -606,65 +613,72 @@ RowLayout { title: qsTr("Monospace font family") expanded: false - StyledListView { + Loader { Layout.fillWidth: true - Layout.preferredHeight: Math.min(contentHeight, 300) - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() - - delegate: StyledRect { - required property string modelData - required property int index - - width: ListView.view.width - - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMono = modelData; - rootPane.saveConfig(); + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: monoFontSection.expanded + + sourceComponent: StyledListView { + id: monoFontList + property alias contentHeight: monoFontList.contentHeight + + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() + + delegate: StyledRect { + required property string modelData + required property int index + + width: ListView.view.width + + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMono = modelData; + rootPane.saveConfig(); + } } - } - RowLayout { - id: fontFamilyMonoRow + RowLayout { + id: fontFamilyMonoRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + } } } } @@ -674,65 +688,72 @@ RowLayout { title: qsTr("Sans-serif font family") expanded: false - StyledListView { + Loader { Layout.fillWidth: true - Layout.preferredHeight: Math.min(contentHeight, 300) - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() - - delegate: StyledRect { - required property string modelData - required property int index - - width: ListView.view.width - - readonly property bool isCurrent: modelData === rootPane.fontFamilySans - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - rootPane.fontFamilySans = modelData; - rootPane.saveConfig(); + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: sansFontSection.expanded + + sourceComponent: StyledListView { + id: sansFontList + property alias contentHeight: sansFontList.contentHeight + + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() + + delegate: StyledRect { + required property string modelData + required property int index + + width: ListView.view.width + + readonly property bool isCurrent: modelData === rootPane.fontFamilySans + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + rootPane.fontFamilySans = modelData; + rootPane.saveConfig(); + } } - } - RowLayout { - id: fontFamilySansRow + RowLayout { + id: fontFamilySansRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Loader { - active: isCurrent - asynchronous: true + Loader { + active: isCurrent + asynchronous: true - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } } - } - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + } } } } diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 71f4d1d..51e991e 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -68,7 +68,7 @@ StyledRect { toggle: false onClicked: { root.visibilities.utilities = false; - root.popouts.detach("appearance"); + root.popouts.detach("network"); } } -- cgit v1.2.3-freya From d751b68bdfcf1113fcab8da0d99748772feaebcd Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 13:15:08 -0500 Subject: controlcenter: corrected missing config.save() on panes --- modules/controlcenter/launcher/LauncherPane.qml | 5 ++++- modules/controlcenter/taskbar/TaskbarPane.qml | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index a7e122e..0cb223b 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -66,8 +66,11 @@ RowLayout { } } - // Update Config to trigger save + // Update Config Config.launcher.hiddenApps = hiddenApps; + + // Persist changes to disk + Config.save(); } onSelectedAppChanged: { diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 6d9761b..8d0e5a0 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -111,6 +111,9 @@ RowLayout { }); } Config.bar.entries = entries; + + // Persist changes to disk + Config.save(); } ListModel { -- cgit v1.2.3-freya From 0e1be8126a4fab3802719ebb3d85f752282be491 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 13:18:28 -0500 Subject: controlcenter: delay heavy operations until animation completes --- modules/controlcenter/Panes.qml | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 5238624..8c76deb 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -49,6 +49,16 @@ ClippingRectangle { y: -root.session.activeIndex * root.height clip: true + property bool animationComplete: true + + Timer { + id: animationDelayTimer + interval: Appearance.anim.durations.normal + onTriggered: { + layout.animationComplete = true; + } + } + Pane { index: 0 sourceComponent: NetworkingPane { @@ -94,6 +104,15 @@ ClippingRectangle { Behavior on y { Anim {} } + + Connections { + target: root.session + function onActiveIndexChanged(): void { + // Mark animation as incomplete and start delay timer + layout.animationComplete = false; + animationDelayTimer.restart(); + } + } } component Pane: Item { @@ -112,10 +131,15 @@ ClippingRectangle { clip: false asynchronous: true active: { - // Keep loaders active for current and adjacent panels - // This prevents content from disappearing during panel transitions const diff = Math.abs(root.session.activeIndex - pane.index); - return diff <= 1; + + // Always activate current and adjacent panes immediately for smooth transitions + if (diff <= 1) { + return true; + } + + // For distant panes, wait until animation completes to avoid heavy loading during transition + return layout.animationComplete; } } } -- cgit v1.2.3-freya From f77323b136a3773266a1140a17e5f7b4cb3f26e7 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 13:21:59 -0500 Subject: controlcenter: lazyloading wallpaper --- .../controlcenter/appearance/AppearancePane.qml | 69 +++++++++++++--------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 09cb04f..fb67ec3 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -1895,34 +1895,47 @@ RowLayout { Item { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.large - Layout.preferredHeight: wallpaperGrid.Layout.preferredHeight + Layout.preferredHeight: wallpaperLoader.item ? wallpaperLoader.item.layoutPreferredHeight : 0 - GridView { - id: wallpaperGrid + Loader { + id: wallpaperLoader anchors.fill: parent - - readonly property int minCellWidth: 200 + Appearance.spacing.normal - readonly property int columnsCount: Math.max(1, Math.floor(parent.width / minCellWidth)) - - Layout.preferredHeight: Math.ceil(count / columnsCount) * cellHeight - height: Layout.preferredHeight - - // Distribute width evenly across columns - cellWidth: width / columnsCount - cellHeight: 140 + Appearance.spacing.normal - - leftMargin: 0 - rightMargin: 0 - topMargin: 0 - bottomMargin: 0 - - model: Wallpapers.list - - // Disable GridView's own scrolling - let parent handle it - interactive: false - - // Enable caching for better performance - cacheBuffer: cellHeight * 2 + asynchronous: true + active: { + // Lazy load: only activate when right pane is loaded + // This defers heavy wallpaper list loading until the right pane is visible + return rightAppearanceLoader.item !== null; + } + + sourceComponent: Item { + property alias layoutPreferredHeight: wallpaperGrid.layoutPreferredHeight + + GridView { + id: wallpaperGrid + anchors.fill: parent + + readonly property int minCellWidth: 200 + Appearance.spacing.normal + readonly property int columnsCount: Math.max(1, Math.floor(parent.width / minCellWidth)) + + readonly property int layoutPreferredHeight: Math.ceil(count / columnsCount) * cellHeight + height: layoutPreferredHeight + + // Distribute width evenly across columns + cellWidth: width / columnsCount + cellHeight: 140 + Appearance.spacing.normal + + leftMargin: 0 + rightMargin: 0 + topMargin: 0 + bottomMargin: 0 + + model: Wallpapers.list + + // Disable GridView's own scrolling - let parent handle it + interactive: false + + // Enable caching for better performance + cacheBuffer: cellHeight * 2 delegate: Item { required property var modelData @@ -2141,7 +2154,9 @@ RowLayout { } } } - } + } + } + } } } } -- cgit v1.2.3-freya From d9034ede8efafc6334f7b2c56b6b3d299c2937fa Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 13:26:11 -0500 Subject: controlcenter: lazyloading applicatoins in launcher pane --- modules/controlcenter/launcher/LauncherPane.qml | 108 +++++++++++++----------- 1 file changed, 61 insertions(+), 47 deletions(-) diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 0cb223b..71d1d6f 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -300,73 +300,87 @@ RowLayout { } } - StyledListView { - id: appsListView + Loader { + id: appsListLoader Layout.fillWidth: true Layout.fillHeight: true + asynchronous: true + active: { + // 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; + } - model: root.filteredApps - spacing: Appearance.spacing.small / 2 - clip: true + sourceComponent: StyledListView { + id: appsListView + + Layout.fillWidth: true + Layout.fillHeight: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } + model: root.filteredApps + spacing: Appearance.spacing.small / 2 + clip: true - delegate: StyledRect { - required property var modelData + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } - width: parent ? parent.width : 0 + delegate: StyledRect { + required property var modelData - readonly property bool isSelected: root.selectedApp === modelData + width: parent ? parent.width : 0 - color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Appearance.rounding.normal + readonly property bool isSelected: root.selectedApp === modelData - opacity: 0 + color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + radius: Appearance.rounding.normal - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic - } - } + opacity: 0 - Component.onCompleted: { - opacity = 1; - } + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic + } + } - StateLayer { - function onClicked(): void { - root.selectedApp = modelData; + Component.onCompleted: { + opacity = 1; } - } - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + StateLayer { + function onClicked(): void { + root.selectedApp = modelData; + } + } - spacing: Appearance.spacing.normal + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + IconImage { + Layout.alignment: Qt.AlignVCenter + implicitSize: 32 + source: { + const entry = modelData.entry; + return entry ? Quickshell.iconPath(entry.icon, "image-missing") : "image-missing"; + } + } - IconImage { - Layout.alignment: Qt.AlignVCenter - implicitSize: 32 - source: { - const entry = modelData.entry; - return entry ? Quickshell.iconPath(entry.icon, "image-missing") : "image-missing"; + StyledText { + Layout.fillWidth: true + text: modelData.name || modelData.entry?.name || qsTr("Unknown") + font.pointSize: Appearance.font.size.normal } } - StyledText { - Layout.fillWidth: true - text: modelData.name || modelData.entry?.name || qsTr("Unknown") - font.pointSize: Appearance.font.size.normal - } + implicitHeight: 40 } - - implicitHeight: 40 } } } -- cgit v1.2.3-freya From 007e94ad33943189cbb8a0ce981b0f737cc89377 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 13:28:29 -0500 Subject: controlcenter: caching and no destoying of panes --- modules/controlcenter/Panes.qml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 8c76deb..28124a0 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -124,6 +124,9 @@ ClippingRectangle { implicitWidth: root.width implicitHeight: root.height + // Track if this pane has ever been loaded to enable caching + property bool hasBeenLoaded: false + Loader { id: loader @@ -135,12 +138,26 @@ ClippingRectangle { // Always activate current and adjacent panes immediately for smooth transitions if (diff <= 1) { + pane.hasBeenLoaded = true; + return true; + } + + // For distant panes that have been loaded before, keep them active to preserve cached data + // Only wait for animation if pane hasn't been loaded yet + if (pane.hasBeenLoaded) { return true; } - // For distant panes, wait until animation completes to avoid heavy loading during transition + // For new distant panes, wait until animation completes to avoid heavy loading during transition return layout.animationComplete; } + + onItemChanged: { + // Mark pane as loaded when item is created + if (item) { + pane.hasBeenLoaded = true; + } + } } } } -- cgit v1.2.3-freya -- cgit v1.2.3-freya From 8a33ad990511d7cce8c68ee416e6999cfa9b8477 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 14:53:20 -0500 Subject: controlcenter: restored floating window toggle --- modules/controlcenter/NavRail.qml | 77 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 8432f17..d1d432d 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config +import qs.modules.controlcenter import Quickshell import QtQuick import QtQuick.Layouts @@ -39,6 +40,82 @@ Item { } } + Loader { + Layout.topMargin: Appearance.spacing.large + asynchronous: true + active: !root.session.floating + visible: active + + sourceComponent: StyledRect { + readonly property int nonAnimWidth: normalWinIcon.implicitWidth + (root.session.navExpanded ? normalWinLabel.anchors.leftMargin + normalWinLabel.implicitWidth : 0) + normalWinIcon.anchors.leftMargin * 2 + + implicitWidth: nonAnimWidth + implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Appearance.padding.normal * 2 : nonAnimWidth + + color: Colours.palette.m3primaryContainer + radius: Appearance.rounding.small + + StateLayer { + id: normalWinState + + color: Colours.palette.m3onPrimaryContainer + + function onClicked(): void { + root.session.root.close(); + WindowFactory.create(null, { + active: root.session.active, + navExpanded: root.session.navExpanded + }); + } + } + + MaterialIcon { + id: normalWinIcon + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.padding.large + + text: "select_window" + color: Colours.palette.m3onPrimaryContainer + font.pointSize: Appearance.font.size.large + fill: 1 + } + + StyledText { + id: normalWinLabel + + anchors.left: normalWinIcon.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.spacing.normal + + text: qsTr("Float window") + color: Colours.palette.m3onPrimaryContainer + opacity: root.session.navExpanded ? 1 : 0 + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.small + } + } + } + + Behavior on implicitWidth { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + } + } + NavItem { Layout.topMargin: Appearance.spacing.large * 2 icon: "router" -- cgit v1.2.3-freya From 18d117557a8e1c29fddc150c6d7cd50b2fc41f2c Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 15:09:58 -0500 Subject: controlcenter: clip/fade issue on certain collapsiblesections --- components/controls/CollapsibleSection.qml | 40 ++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 2df169a..7cb14a0 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -70,8 +70,15 @@ ColumnLayout { Item { Layout.fillWidth: true - Layout.preferredHeight: (root.expanded && root.description !== "") ? descriptionText.implicitHeight + Appearance.spacing.smaller + Appearance.spacing.small : 0 - clip: true + Layout.preferredHeight: (root.expanded && root.description !== "") ? Math.min(descriptionText.implicitHeight + Appearance.spacing.smaller + Appearance.spacing.small, maxDescriptionHeight) : 0 + + readonly property real maxDescriptionHeight: 60 + + layer.enabled: true + layer.smooth: true + layer.effect: OpacityMask { + maskSource: descriptionMask + } Behavior on Layout.preferredHeight { Anim { @@ -79,6 +86,34 @@ ColumnLayout { } } + Item { + id: descriptionMask + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { + anchors.fill: parent + + gradient: Gradient { + orientation: Gradient.Vertical + + GradientStop { + position: 0.0 + color: Qt.rgba(0, 0, 0, 1) + } + GradientStop { + position: 0.7 + color: Qt.rgba(0, 0, 0, 1) + } + GradientStop { + position: 1.0 + color: Qt.rgba(0, 0, 0, 0) + } + } + } + } + StyledText { id: descriptionText anchors.left: parent.left @@ -90,6 +125,7 @@ ColumnLayout { text: root.description color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small + wrapMode: Text.Wrap opacity: (root.expanded && root.description !== "") ? 1.0 : 0.0 Behavior on opacity { -- cgit v1.2.3-freya From b3d54fa0737bef45d04a049178ce8dba54ffbfcf Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 16:49:54 -0500 Subject: controlcenter: animation correction on certain collapsiblesections --- components/controls/CollapsibleSection.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 7cb14a0..aa206ba 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -154,10 +154,9 @@ ColumnLayout { id: contentColumn anchors.left: parent.left anchors.right: parent.right - anchors.top: parent.top + y: Appearance.spacing.small anchors.leftMargin: Appearance.padding.normal anchors.rightMargin: Appearance.padding.normal - anchors.topMargin: Appearance.spacing.small anchors.bottomMargin: Appearance.spacing.small spacing: Appearance.spacing.small opacity: root.expanded ? 1.0 : 0.0 -- cgit v1.2.3-freya From d2373e03a5cbd0cb97e9c5e32966a0c0afd80f10 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 19:07:51 -0500 Subject: controlcenter: FIXED animation bug with sub-text --- components/controls/CollapsibleSection.qml | 80 +++++------------------------- 1 file changed, 12 insertions(+), 68 deletions(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index aa206ba..35acdec 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -68,74 +68,6 @@ ColumnLayout { } } - Item { - Layout.fillWidth: true - Layout.preferredHeight: (root.expanded && root.description !== "") ? Math.min(descriptionText.implicitHeight + Appearance.spacing.smaller + Appearance.spacing.small, maxDescriptionHeight) : 0 - - readonly property real maxDescriptionHeight: 60 - - layer.enabled: true - layer.smooth: true - layer.effect: OpacityMask { - maskSource: descriptionMask - } - - Behavior on Layout.preferredHeight { - Anim { - easing.bezierCurve: Appearance.anim.curves.standard - } - } - - Item { - id: descriptionMask - anchors.fill: parent - layer.enabled: true - visible: false - - Rectangle { - anchors.fill: parent - - gradient: Gradient { - orientation: Gradient.Vertical - - GradientStop { - position: 0.0 - color: Qt.rgba(0, 0, 0, 1) - } - GradientStop { - position: 0.7 - color: Qt.rgba(0, 0, 0, 1) - } - GradientStop { - position: 1.0 - color: Qt.rgba(0, 0, 0, 0) - } - } - } - } - - StyledText { - id: descriptionText - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.leftMargin: Appearance.padding.normal - anchors.rightMargin: Appearance.padding.normal - anchors.topMargin: Appearance.spacing.smaller - text: root.description - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small - wrapMode: Text.Wrap - opacity: (root.expanded && root.description !== "") ? 1.0 : 0.0 - - Behavior on opacity { - Anim { - easing.bezierCurve: Appearance.anim.curves.standard - } - } - } - } - default property alias content: contentColumn.data Item { @@ -166,6 +98,18 @@ ColumnLayout { easing.bezierCurve: Appearance.anim.curves.standard } } + + StyledText { + id: descriptionText + Layout.fillWidth: true + Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0 + Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0 + visible: root.description !== "" + text: root.description + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + wrapMode: Text.Wrap + } } } } -- cgit v1.2.3-freya From d0c51d4c51e91614cf14b9b32de518ea0f81d228 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 19:22:46 -0500 Subject: hierarchy: moved ethernet to network --- modules/controlcenter/ethernet/EthernetDetails.qml | 119 --------------- modules/controlcenter/ethernet/EthernetList.qml | 168 --------------------- modules/controlcenter/ethernet/EthernetPane.qml | 150 ------------------ .../controlcenter/ethernet/EthernetSettings.qml | 84 ----------- modules/controlcenter/network/EthernetDetails.qml | 119 +++++++++++++++ modules/controlcenter/network/EthernetList.qml | 168 +++++++++++++++++++++ modules/controlcenter/network/EthernetPane.qml | 150 ++++++++++++++++++ modules/controlcenter/network/EthernetSettings.qml | 84 +++++++++++ modules/controlcenter/network/NetworkingPane.qml | 1 - 9 files changed, 521 insertions(+), 522 deletions(-) delete mode 100644 modules/controlcenter/ethernet/EthernetDetails.qml delete mode 100644 modules/controlcenter/ethernet/EthernetList.qml delete mode 100644 modules/controlcenter/ethernet/EthernetPane.qml delete mode 100644 modules/controlcenter/ethernet/EthernetSettings.qml create mode 100644 modules/controlcenter/network/EthernetDetails.qml create mode 100644 modules/controlcenter/network/EthernetList.qml create mode 100644 modules/controlcenter/network/EthernetPane.qml create mode 100644 modules/controlcenter/network/EthernetSettings.qml diff --git a/modules/controlcenter/ethernet/EthernetDetails.qml b/modules/controlcenter/ethernet/EthernetDetails.qml deleted file mode 100644 index f715970..0000000 --- a/modules/controlcenter/ethernet/EthernetDetails.qml +++ /dev/null @@ -1,119 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import qs.components -import qs.components.controls -import qs.components.effects -import qs.components.containers -import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts - -Item { - id: root - - required property Session session - readonly property var device: session.ethernet.active - - Component.onCompleted: { - if (device && device.interface) { - Nmcli.getEthernetDeviceDetails(device.interface, () => {}); - } - } - - onDeviceChanged: { - if (device && device.interface) { - Nmcli.getEthernetDeviceDetails(device.interface, () => {}); - } else { - Nmcli.ethernetDeviceDetails = null; - } - } - - StyledFlickable { - id: flickable - - anchors.fill: parent - - flickableDirection: Flickable.VerticalFlick - clip: true - contentHeight: layout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: flickable - } - - ColumnLayout { - id: layout - - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.normal - - ConnectionHeader { - icon: "cable" - title: root.device?.interface ?? qsTr("Unknown") - } - - SectionHeader { - title: qsTr("Connection status") - description: qsTr("Connection settings for this device") - } - - SectionContainer { - ToggleRow { - label: qsTr("Connected") - checked: root.device?.connected ?? false - toggle.onToggled: { - if (checked) { - Nmcli.connectEthernet(root.device?.connection || "", root.device?.interface || "", () => {}); - } else { - if (root.device?.connection) { - Nmcli.disconnectEthernet(root.device.connection, () => {}); - } - } - } - } - } - - SectionHeader { - title: qsTr("Device properties") - description: qsTr("Additional information") - } - - SectionContainer { - contentSpacing: Appearance.spacing.small / 2 - - PropertyRow { - label: qsTr("Interface") - value: root.device?.interface ?? qsTr("Unknown") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Connection") - value: root.device?.connection || qsTr("Not connected") - } - - PropertyRow { - showTopMargin: true - label: qsTr("State") - value: root.device?.state ?? qsTr("Unknown") - } - } - - SectionHeader { - title: qsTr("Connection information") - description: qsTr("Network connection details") - } - - SectionContainer { - ConnectionInfoSection { - deviceDetails: Nmcli.ethernetDeviceDetails - } - } - - } - } - -} \ No newline at end of file diff --git a/modules/controlcenter/ethernet/EthernetList.qml b/modules/controlcenter/ethernet/EthernetList.qml deleted file mode 100644 index 45c9481..0000000 --- a/modules/controlcenter/ethernet/EthernetList.qml +++ /dev/null @@ -1,168 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import qs.components -import qs.components.controls -import qs.components.containers -import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts - -ColumnLayout { - id: root - - required property Session session - - spacing: Appearance.spacing.small - - RowLayout { - spacing: Appearance.spacing.smaller - - StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - ToggleButton { - toggled: !root.session.ethernet.active - icon: "settings" - accent: "Primary" - - onClicked: { - if (root.session.ethernet.active) - root.session.ethernet.active = null; - else { - root.session.ethernet.active = view.model.get(0)?.modelData ?? null; - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - } - - StyledText { - text: qsTr("All available ethernet devices") - color: Colours.palette.m3outline - } - - StyledListView { - id: view - - Layout.fillWidth: true - Layout.fillHeight: true - - model: Nmcli.ethernetDevices - - spacing: Appearance.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: view - } - - delegate: StyledRect { - required property var modelData - - anchors.left: parent.left - anchors.right: parent.right - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: root.session.ethernet.active === modelData ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - root.session.ethernet.active = modelData; - } - } - - RowLayout { - id: rowLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 - - radius: Appearance.rounding.normal - color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - - MaterialIcon { - id: icon - - anchors.centerIn: parent - text: "cable" - font.pointSize: Appearance.font.size.large - fill: modelData.connected ? 1 : 0 - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - } - - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - - text: modelData.interface || qsTr("Unknown") - } - - StyledText { - text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") - color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: modelData.connected ? 500 : 400 - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 - - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) - - StateLayer { - function onClicked(): void { - if (modelData.connected && modelData.connection) { - Nmcli.disconnectEthernet(modelData.connection, () => {}); - } else { - Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); - } - } - } - - MaterialIcon { - id: connectIcon - - anchors.centerIn: parent - text: modelData.connected ? "link_off" : "link" - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - } - } - - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 - } - } -} \ No newline at end of file diff --git a/modules/controlcenter/ethernet/EthernetPane.qml b/modules/controlcenter/ethernet/EthernetPane.qml deleted file mode 100644 index 05d0b1b..0000000 --- a/modules/controlcenter/ethernet/EthernetPane.qml +++ /dev/null @@ -1,150 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import qs.components -import qs.components.effects -import qs.components.containers -import qs.config -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts - -RowLayout { - id: root - - required property Session session - - anchors.fill: parent - - spacing: 0 - - Item { - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - EthernetList { - 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 - - session: root.session - } - - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: rightBorder.innerRadius - color: "transparent" - clip: true - - Loader { - id: loader - - property var pane: root.session.ethernet.active - property string paneId: pane ? (pane.interface || "") : "" - - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - - opacity: 1 - scale: 1 - transformOrigin: Item.Center - - clip: true - asynchronous: true - sourceComponent: pane ? details : settings - - 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 - } - } - PropertyAction {} - 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 - } - } - } - } - - onPaneChanged: { - paneId = pane ? (pane.interface || "") : ""; - } - } - } - - InnerBorder { - id: rightBorder - - leftThickness: Appearance.padding.normal / 2 - } - - Component { - id: settings - - StyledFlickable { - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - clip: true - - EthernetSettings { - id: settingsInner - - anchors.left: parent.left - anchors.right: parent.right - session: root.session - } - } - } - - Component { - id: details - - EthernetDetails { - session: root.session - } - } - } - - component Anim: NumberAnimation { - target: loader - duration: Appearance.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - } -} \ No newline at end of file diff --git a/modules/controlcenter/ethernet/EthernetSettings.qml b/modules/controlcenter/ethernet/EthernetSettings.qml deleted file mode 100644 index 161492c..0000000 --- a/modules/controlcenter/ethernet/EthernetSettings.qml +++ /dev/null @@ -1,84 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import qs.components -import qs.components.controls -import qs.components.effects -import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts - -ColumnLayout { - id: root - - required property Session session - - spacing: Appearance.spacing.normal - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "cable" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Ethernet settings") - font.pointSize: Appearance.font.size.large - font.bold: true - } - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Ethernet devices") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("Available ethernet devices") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: ethernetInfo - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.small / 2 - - StyledText { - text: qsTr("Total devices") - } - - StyledText { - text: qsTr("%1").arg(Nmcli.ethernetDevices.length) - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Connected devices") - } - - StyledText { - text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - } - } -} \ No newline at end of file diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml new file mode 100644 index 0000000..f715970 --- /dev/null +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -0,0 +1,119 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Session session + readonly property var device: session.ethernet.active + + Component.onCompleted: { + if (device && device.interface) { + Nmcli.getEthernetDeviceDetails(device.interface, () => {}); + } + } + + onDeviceChanged: { + if (device && device.interface) { + Nmcli.getEthernetDeviceDetails(device.interface, () => {}); + } else { + Nmcli.ethernetDeviceDetails = null; + } + } + + StyledFlickable { + id: flickable + + anchors.fill: parent + + flickableDirection: Flickable.VerticalFlick + clip: true + contentHeight: layout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: flickable + } + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + + ConnectionHeader { + icon: "cable" + title: root.device?.interface ?? qsTr("Unknown") + } + + SectionHeader { + title: qsTr("Connection status") + description: qsTr("Connection settings for this device") + } + + SectionContainer { + ToggleRow { + label: qsTr("Connected") + checked: root.device?.connected ?? false + toggle.onToggled: { + if (checked) { + Nmcli.connectEthernet(root.device?.connection || "", root.device?.interface || "", () => {}); + } else { + if (root.device?.connection) { + Nmcli.disconnectEthernet(root.device.connection, () => {}); + } + } + } + } + } + + SectionHeader { + title: qsTr("Device properties") + description: qsTr("Additional information") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Interface") + value: root.device?.interface ?? qsTr("Unknown") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Connection") + value: root.device?.connection || qsTr("Not connected") + } + + PropertyRow { + showTopMargin: true + label: qsTr("State") + value: root.device?.state ?? qsTr("Unknown") + } + } + + SectionHeader { + title: qsTr("Connection information") + description: qsTr("Network connection details") + } + + SectionContainer { + ConnectionInfoSection { + deviceDetails: Nmcli.ethernetDeviceDetails + } + } + + } + } + +} \ No newline at end of file diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml new file mode 100644 index 0000000..45c9481 --- /dev/null +++ b/modules/controlcenter/network/EthernetList.qml @@ -0,0 +1,168 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + ToggleButton { + toggled: !root.session.ethernet.active + icon: "settings" + accent: "Primary" + + onClicked: { + if (root.session.ethernet.active) + root.session.ethernet.active = null; + else { + root.session.ethernet.active = view.model.get(0)?.modelData ?? null; + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + } + + StyledText { + text: qsTr("All available ethernet devices") + color: Colours.palette.m3outline + } + + StyledListView { + id: view + + Layout.fillWidth: true + Layout.fillHeight: true + + model: Nmcli.ethernetDevices + + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: view + } + + delegate: StyledRect { + required property var modelData + + anchors.left: parent.left + anchors.right: parent.right + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: root.session.ethernet.active === modelData ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + root.session.ethernet.active = modelData; + } + } + + RowLayout { + id: rowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: "cable" + font.pointSize: Appearance.font.size.large + fill: modelData.connected ? 1 : 0 + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + + text: modelData.interface || qsTr("Unknown") + } + + StyledText { + text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.connected ? 500 : 400 + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) + + StateLayer { + function onClicked(): void { + if (modelData.connected && modelData.connection) { + Nmcli.disconnectEthernet(modelData.connection, () => {}); + } else { + Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); + } + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + text: modelData.connected ? "link_off" : "link" + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + } + + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } +} \ No newline at end of file diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml new file mode 100644 index 0000000..05d0b1b --- /dev/null +++ b/modules/controlcenter/network/EthernetPane.qml @@ -0,0 +1,150 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.effects +import qs.components.containers +import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property Session session + + anchors.fill: parent + + spacing: 0 + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true + + EthernetList { + 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 + + session: root.session + } + + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ClippingRectangle { + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + + radius: rightBorder.innerRadius + color: "transparent" + clip: true + + Loader { + id: loader + + property var pane: root.session.ethernet.active + property string paneId: pane ? (pane.interface || "") : "" + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + + clip: true + asynchronous: true + sourceComponent: pane ? details : settings + + 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 + } + } + PropertyAction {} + 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 + } + } + } + } + + onPaneChanged: { + paneId = pane ? (pane.interface || "") : ""; + } + } + } + + InnerBorder { + id: rightBorder + + leftThickness: Appearance.padding.normal / 2 + } + + Component { + id: settings + + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + clip: true + + EthernetSettings { + id: settingsInner + + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } + } + } + + Component { + id: details + + EthernetDetails { + session: root.session + } + } + } + + component Anim: NumberAnimation { + target: loader + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + } +} \ No newline at end of file diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml new file mode 100644 index 0000000..161492c --- /dev/null +++ b/modules/controlcenter/network/EthernetSettings.qml @@ -0,0 +1,84 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "cable" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Ethernet settings") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Ethernet devices") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Available ethernet devices") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: ethernetInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("Total devices") + } + + StyledText { + text: qsTr("%1").arg(Nmcli.ethernetDevices.length) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Connected devices") + } + + StyledText { + text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + } + } +} \ No newline at end of file diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 1dd798d..3601b8b 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -1,7 +1,6 @@ pragma ComponentBehavior: Bound import ".." -import "../ethernet" import "." import qs.components import qs.components.controls -- cgit v1.2.3-freya From 07637da8f6ad4a28670d9eeb70f83ae8889c0a17 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 19:31:22 -0500 Subject: controlcenter: removed accordion auto-close on sections --- .../controlcenter/appearance/AppearancePane.qml | 47 ---------------------- 1 file changed, 47 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index fb67ec3..5f70abb 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -130,26 +130,6 @@ RowLayout { flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.height - function collapseAllSections(exceptSection) { - if (exceptSection !== themeModeSection) - themeModeSection.expanded = false; - if (exceptSection !== colorVariantSection) - colorVariantSection.expanded = false; - if (exceptSection !== colorSchemeSection) - colorSchemeSection.expanded = false; - if (exceptSection !== animationsSection) - animationsSection.expanded = false; - if (exceptSection !== fontsSection) - fontsSection.expanded = false; - if (exceptSection !== scalesSection) - scalesSection.expanded = false; - if (exceptSection !== transparencySection) - transparencySection.expanded = false; - if (exceptSection !== borderSection) - borderSection.expanded = false; - if (exceptSection !== backgroundSection) - backgroundSection.expanded = false; - } StyledScrollBar.vertical: StyledScrollBar { flickable: sidebarFlickable @@ -179,9 +159,6 @@ RowLayout { id: themeModeSection title: qsTr("Theme mode") description: qsTr("Light or dark theme") - onToggleRequested: { - sidebarFlickable.collapseAllSections(themeModeSection); - } SwitchRow { label: qsTr("Dark mode") @@ -196,9 +173,6 @@ RowLayout { id: colorVariantSection title: qsTr("Color variant") description: qsTr("Material theme variant") - onToggleRequested: { - sidebarFlickable.collapseAllSections(colorVariantSection); - } ColumnLayout { Layout.fillWidth: true @@ -282,9 +256,6 @@ RowLayout { id: colorSchemeSection title: qsTr("Color scheme") description: qsTr("Available color schemes") - onToggleRequested: { - sidebarFlickable.collapseAllSections(colorSchemeSection); - } ColumnLayout { Layout.fillWidth: true @@ -422,9 +393,6 @@ RowLayout { CollapsibleSection { id: animationsSection title: qsTr("Animations") - onToggleRequested: { - sidebarFlickable.collapseAllSections(animationsSection); - } SectionContainer { contentSpacing: Appearance.spacing.normal @@ -529,9 +497,6 @@ RowLayout { CollapsibleSection { id: fontsSection title: qsTr("Fonts") - onToggleRequested: { - sidebarFlickable.collapseAllSections(fontsSection); - } CollapsibleSection { id: materialFontSection @@ -861,9 +826,6 @@ RowLayout { CollapsibleSection { id: scalesSection title: qsTr("Scales") - onToggleRequested: { - sidebarFlickable.collapseAllSections(scalesSection); - } SectionContainer { contentSpacing: Appearance.spacing.normal @@ -1166,9 +1128,6 @@ RowLayout { CollapsibleSection { id: transparencySection title: qsTr("Transparency") - onToggleRequested: { - sidebarFlickable.collapseAllSections(transparencySection); - } SwitchRow { label: qsTr("Transparency enabled") @@ -1381,9 +1340,6 @@ RowLayout { CollapsibleSection { id: borderSection title: qsTr("Border") - onToggleRequested: { - sidebarFlickable.collapseAllSections(borderSection); - } SectionContainer { contentSpacing: Appearance.spacing.normal @@ -1575,9 +1531,6 @@ RowLayout { CollapsibleSection { id: backgroundSection title: qsTr("Background") - onToggleRequested: { - sidebarFlickable.collapseAllSections(backgroundSection); - } SwitchRow { label: qsTr("Desktop clock") -- cgit v1.2.3-freya From 8981ab8806609496360d11cf34384fc337368ff7 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 19:36:05 -0500 Subject: controlcenter: added collapse/expand all to apperaance and taskbar --- .../controlcenter/appearance/AppearancePane.qml | 29 +++++++++++++++ modules/controlcenter/taskbar/TaskbarPane.qml | 41 ++++++++++++++++------ 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 5f70abb..2b3bbce 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -141,6 +141,17 @@ RowLayout { anchors.right: parent.right spacing: Appearance.spacing.small + readonly property bool allSectionsExpanded: + themeModeSection.expanded && + colorVariantSection.expanded && + colorSchemeSection.expanded && + animationsSection.expanded && + fontsSection.expanded && + scalesSection.expanded && + transparencySection.expanded && + borderSection.expanded && + backgroundSection.expanded + RowLayout { spacing: Appearance.spacing.smaller @@ -153,6 +164,24 @@ RowLayout { Item { Layout.fillWidth: true } + + IconButton { + icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more" + type: IconButton.Text + label.animate: true + onClicked: { + const shouldExpand = !sidebarLayout.allSectionsExpanded; + themeModeSection.expanded = shouldExpand; + colorVariantSection.expanded = shouldExpand; + colorSchemeSection.expanded = shouldExpand; + animationsSection.expanded = shouldExpand; + fontsSection.expanded = shouldExpand; + scalesSection.expanded = shouldExpand; + transparencySection.expanded = shouldExpand; + borderSection.expanded = shouldExpand; + backgroundSection.expanded = shouldExpand; + } + } } CollapsibleSection { diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 8d0e5a0..c731acc 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -175,19 +175,40 @@ RowLayout { spacing: Appearance.spacing.small + readonly property bool allSectionsExpanded: + clockSection.expanded && + barBehaviorSection.expanded && + statusIconsSection.expanded && + traySettingsSection.expanded && + workspacesSection.expanded + RowLayout { - spacing: Appearance.spacing.smaller + spacing: Appearance.spacing.smaller - StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } - Item { - Layout.fillWidth: true - } - } + Item { + Layout.fillWidth: true + } + + IconButton { + icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more" + type: IconButton.Text + label.animate: true + onClicked: { + const shouldExpand = !sidebarLayout.allSectionsExpanded; + clockSection.expanded = shouldExpand; + barBehaviorSection.expanded = shouldExpand; + statusIconsSection.expanded = shouldExpand; + traySettingsSection.expanded = shouldExpand; + workspacesSection.expanded = shouldExpand; + } + } + } CollapsibleSection { id: clockSection -- cgit v1.2.3-freya From cdd522d01d97ebc43b334d50605ace36c457dbaa Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 19:48:32 -0500 Subject: controlcenter: deferred pane loading and limited simultaneous pane loading --- modules/controlcenter/Panes.qml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 28124a0..e97a3fe 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -50,6 +50,9 @@ ClippingRectangle { clip: true property bool animationComplete: true + // Track if initial opening animation has completed + // During initial opening, only the active pane loads to avoid hiccups + property bool initialOpeningComplete: false Timer { id: animationDelayTimer @@ -59,6 +62,17 @@ ClippingRectangle { } } + // Timer to detect when initial opening animation completes + // Uses large duration to cover both normal and detached opening cases + Timer { + id: initialOpeningTimer + interval: Appearance.anim.durations.large + running: true + onTriggered: { + layout.initialOpeningComplete = true; + } + } + Pane { index: 0 sourceComponent: NetworkingPane { @@ -135,8 +149,20 @@ ClippingRectangle { asynchronous: true active: { const diff = Math.abs(root.session.activeIndex - pane.index); + const isActivePane = diff === 0; + + // During initial opening animation, only load the active pane + // This prevents hiccups from multiple panes loading simultaneously + if (!layout.initialOpeningComplete) { + if (isActivePane) { + pane.hasBeenLoaded = true; + return true; + } + // Defer all other panes until initial opening completes + return false; + } - // Always activate current and adjacent panes immediately for smooth transitions + // After initial opening, allow current and adjacent panes for smooth transitions if (diff <= 1) { pane.hasBeenLoaded = true; return true; -- cgit v1.2.3-freya From 84638bfdb83fe653fbe4144e94efff0c5a28ecec Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 20:03:35 -0500 Subject: hierarchy: renamed for consistency --- modules/bar/popouts/Content.qml | 2 +- modules/bar/popouts/WirelessPassword.qml | 604 +++++++++++++++++++++++++ modules/bar/popouts/WirelessPasswordPopout.qml | 603 ------------------------ 3 files changed, 605 insertions(+), 604 deletions(-) create mode 100644 modules/bar/popouts/WirelessPassword.qml delete mode 100644 modules/bar/popouts/WirelessPasswordPopout.qml diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 7561eec..da993fa 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -51,7 +51,7 @@ Item { Popout { id: passwordPopout name: "wirelesspassword" - sourceComponent: WirelessPasswordPopout { + sourceComponent: WirelessPassword { id: passwordComponent wrapper: root.wrapper network: networkPopout.item?.passwordNetwork ?? null diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml new file mode 100644 index 0000000..a9d098e --- /dev/null +++ b/modules/bar/popouts/WirelessPassword.qml @@ -0,0 +1,604 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.controls +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Item wrapper + property var network: null + property bool isClosing: false + + readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" + + Connections { + target: root.wrapper + function onCurrentNameChanged() { + if (root.wrapper.currentName === "wirelesspassword") { + // Update network when popout becomes active + Qt.callLater(() => { + // Try to get network from parent Content's networkPopout + const content = root.parent?.parent?.parent; + if (content) { + const networkPopout = content.children.find(c => c.name === "network"); + if (networkPopout && networkPopout.item) { + root.network = networkPopout.item.passwordNetwork; + } + } + // Force focus to password container when popout becomes active + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + }); + } + } + } + + Timer { + id: focusTimer + interval: 150 + onTriggered: { + root.forceActiveFocus(); + passwordContainer.forceActiveFocus(); + } + } + + spacing: Appearance.spacing.normal + + implicitWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + + visible: shouldBeVisible || isClosing + enabled: shouldBeVisible && !isClosing + focus: enabled + + Component.onCompleted: { + if (shouldBeVisible) { + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + } + } + + onShouldBeVisibleChanged: { + if (shouldBeVisible) { + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + } + } + + Keys.onEscapePressed: closeDialog() + + StyledRect { + Layout.fillWidth: true + Layout.preferredWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surface + opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 + scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + ParallelAnimation { + running: root.isClosing + onFinished: { + if (root.isClosing) { + root.isClosing = false; + } + } + + Anim { + target: parent + property: "opacity" + to: 0 + } + Anim { + target: parent + property: "scale" + to: 0.7 + } + } + + Keys.onEscapePressed: root.closeDialog() + + ColumnLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "lock" + font.pointSize: Appearance.font.size.extraLarge * 2 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enter password") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + id: networkNameText + Layout.alignment: Qt.AlignHCenter + text: { + if (root.network) { + const ssid = root.network.ssid; + if (ssid && ssid.length > 0) { + return qsTr("Network: %1").arg(ssid); + } + } + return qsTr("Network: Unknown"); + } + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + Timer { + interval: 50 + running: root.shouldBeVisible && (!root.network || !root.network.ssid) + repeat: true + property int attempts: 0 + onTriggered: { + attempts++; + // Keep trying to get network from Network component + const content = root.parent?.parent?.parent; + if (content) { + const networkPopout = content.children.find(c => c.name === "network"); + if (networkPopout && networkPopout.item && networkPopout.item.passwordNetwork) { + root.network = networkPopout.item.passwordNetwork; + } + } + // Stop if we got it or after 20 attempts (1 second) + if ((root.network && root.network.ssid) || attempts >= 20) { + stop(); + attempts = 0; + } + } + onRunningChanged: { + if (!running) { + attempts = 0; + } + } + } + + StyledText { + id: statusText + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.spacing.small + visible: connectButton.connecting || connectButton.hasError + text: { + if (connectButton.hasError) { + return qsTr("Connection failed. Please check your password and try again."); + } + if (connectButton.connecting) { + return qsTr("Connecting..."); + } + return ""; + } + color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + font.weight: 400 + wrapMode: Text.WordWrap + Layout.maximumWidth: parent.width - Appearance.padding.large * 2 + } + + FocusScope { + id: passwordContainer + objectName: "passwordContainer" + Layout.topMargin: Appearance.spacing.large + Layout.fillWidth: true + implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + + focus: true + activeFocusOnTab: true + + property string passwordBuffer: "" + + Keys.onPressed: event => { + // Ensure we have focus when receiving keyboard input + if (!activeFocus) { + forceActiveFocus(); + } + + // Clear error when user starts typing + if (connectButton.hasError && event.text && event.text.length > 0) { + connectButton.hasError = false; + } + + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { + if (connectButton.enabled) { + connectButton.clicked(); + } + event.accepted = true; + } else if (event.key === Qt.Key_Backspace) { + if (event.modifiers & Qt.ControlModifier) { + passwordBuffer = ""; + } else { + passwordBuffer = passwordBuffer.slice(0, -1); + } + event.accepted = true; + } else if (event.text && event.text.length > 0) { + passwordBuffer += event.text; + event.accepted = true; + } + } + + Connections { + target: root + function onShouldBeVisibleChanged(): void { + if (root.shouldBeVisible) { + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); + passwordContainer.passwordBuffer = ""; + connectButton.hasError = false; + } + } + } + + Timer { + id: passwordFocusTimer + interval: 50 + onTriggered: { + passwordContainer.forceActiveFocus(); + } + } + + Component.onCompleted: { + if (root.shouldBeVisible) { + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); + } + } + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.normal + color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer + border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) + border.color: { + if (connectButton.hasError) { + return Colours.palette.m3error; + } + if (passwordContainer.activeFocus) { + return Colours.palette.m3primary; + } + return root.shouldBeVisible ? Colours.palette.m3outline : "transparent"; + } + + Behavior on border.color { + CAnim {} + } + + Behavior on border.width { + CAnim {} + } + + Behavior on color { + CAnim {} + } + } + + StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor + radius: Appearance.rounding.normal + + function onClicked(): void { + passwordContainer.forceActiveFocus(); + } + } + + StyledText { + id: placeholder + + anchors.centerIn: parent + text: qsTr("Password") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + font.family: Appearance.font.family.mono + opacity: passwordContainer.passwordBuffer ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + + ListView { + id: charList + + readonly property int fullWidth: count * (implicitHeight + spacing) - spacing + + anchors.centerIn: parent + implicitWidth: fullWidth + implicitHeight: Appearance.font.size.normal + + orientation: Qt.Horizontal + spacing: Appearance.spacing.small / 2 + interactive: false + + model: ScriptModel { + values: passwordContainer.passwordBuffer.split("") + } + + delegate: StyledRect { + id: ch + + implicitWidth: implicitHeight + implicitHeight: charList.implicitHeight + + color: Colours.palette.m3onSurface + radius: Appearance.rounding.small / 2 + + opacity: 0 + scale: 0 + Component.onCompleted: { + opacity = 1; + scale = 1; + } + ListView.onRemove: removeAnim.start() + + SequentialAnimation { + id: removeAnim + + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: true + } + ParallelAnimation { + Anim { + target: ch + property: "opacity" + to: 0 + } + Anim { + target: ch + property: "scale" + to: 0.5 + } + } + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: false + } + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } + + Behavior on implicitWidth { + Anim {} + } + } + } + + RowLayout { + Layout.topMargin: Appearance.spacing.normal + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + TextButton { + id: cancelButton + + Layout.fillWidth: true + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + inactiveColour: Colours.palette.m3secondaryContainer + inactiveOnColour: Colours.palette.m3onSecondaryContainer + text: qsTr("Cancel") + + onClicked: root.closeDialog() + } + + TextButton { + id: connectButton + + property bool connecting: false + property bool hasError: false + + Layout.fillWidth: true + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + inactiveColour: Colours.palette.m3primary + inactiveOnColour: Colours.palette.m3onPrimary + text: qsTr("Connect") + enabled: passwordContainer.passwordBuffer.length > 0 && !connecting + + onClicked: { + if (!root.network || connecting) { + return; + } + + const password = passwordContainer.passwordBuffer; + if (!password || password.length === 0) { + return; + } + + // Clear any previous error + hasError = false; + + // Set connecting state + connecting = true; + enabled = false; + text = qsTr("Connecting..."); + + // Connect to network + Nmcli.connectToNetwork(root.network.ssid, password, root.network.bssid || "", result => { + if (result && result.success) + // Connection successful, monitor will handle the rest + {} else if (result && result.needsPassword) { + // Shouldn't happen since we provided password + connectionMonitor.stop(); + connecting = false; + hasError = true; + enabled = true; + text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } else { + // Connection failed immediately - show error + connectionMonitor.stop(); + connecting = false; + hasError = true; + enabled = true; + text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + }); + + // Start monitoring connection + connectionMonitor.start(); + } + } + } + } + } + + function checkConnectionStatus(): void { + if (!root.shouldBeVisible || !connectButton.connecting) { + return; + } + + // Check if we're connected to the target network (case-insensitive SSID comparison) + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + + if (isConnected) { + // Successfully connected - give it a moment for network list to update + // Use Timer for actual delay + connectionSuccessTimer.start(); + return; + } + + // Check for connection failures - if pending connection was cleared but we're not connected + if (Nmcli.pendingConnection === null && connectButton.connecting) { + // Wait a bit more before giving up (allow time for connection to establish) + if (connectionMonitor.repeatCount > 10) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + } + } + + Timer { + id: connectionMonitor + interval: 1000 + repeat: true + triggeredOnStart: false + property int repeatCount: 0 + + onTriggered: { + repeatCount++; + root.checkConnectionStatus(); + } + + onRunningChanged: { + if (!running) { + repeatCount = 0; + } + } + } + + Timer { + id: connectionSuccessTimer + interval: 500 + onTriggered: { + // Double-check connection is still active + if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { + const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + // Return to network popout on successful connection + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + closeDialog(); + } + } + } + } + + Connections { + target: Nmcli + function onActiveChanged() { + if (root.shouldBeVisible) { + root.checkConnectionStatus(); + } + } + function onConnectionFailed(ssid: string) { + if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + Nmcli.forgetNetwork(ssid); + } + } + } + + function closeDialog(): void { + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; + connectButton.connecting = false; + connectButton.hasError = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); + + // Return to network popout + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + } +} + diff --git a/modules/bar/popouts/WirelessPasswordPopout.qml b/modules/bar/popouts/WirelessPasswordPopout.qml deleted file mode 100644 index 59a15b9..0000000 --- a/modules/bar/popouts/WirelessPasswordPopout.qml +++ /dev/null @@ -1,603 +0,0 @@ -pragma ComponentBehavior: Bound - -import qs.components -import qs.components.controls -import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts - -ColumnLayout { - id: root - - required property Item wrapper - property var network: null - property bool isClosing: false - - readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" - - Connections { - target: root.wrapper - function onCurrentNameChanged() { - if (root.wrapper.currentName === "wirelesspassword") { - // Update network when popout becomes active - Qt.callLater(() => { - // Try to get network from parent Content's networkPopout - const content = root.parent?.parent?.parent; - if (content) { - const networkPopout = content.children.find(c => c.name === "network"); - if (networkPopout && networkPopout.item) { - root.network = networkPopout.item.passwordNetwork; - } - } - // Force focus to password container when popout becomes active - // Use Timer for actual delay to ensure dialog is fully rendered - focusTimer.start(); - }); - } - } - } - - Timer { - id: focusTimer - interval: 150 - onTriggered: { - root.forceActiveFocus(); - passwordContainer.forceActiveFocus(); - } - } - - spacing: Appearance.spacing.normal - - implicitWidth: 400 - implicitHeight: content.implicitHeight + Appearance.padding.large * 2 - - visible: shouldBeVisible || isClosing - enabled: shouldBeVisible && !isClosing - focus: enabled - - Component.onCompleted: { - if (shouldBeVisible) { - // Use Timer for actual delay to ensure dialog is fully rendered - focusTimer.start(); - } - } - - onShouldBeVisibleChanged: { - if (shouldBeVisible) { - // Use Timer for actual delay to ensure dialog is fully rendered - focusTimer.start(); - } - } - - Keys.onEscapePressed: closeDialog() - - StyledRect { - Layout.fillWidth: true - Layout.preferredWidth: 400 - implicitHeight: content.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surface - opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 - scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim {} - } - - ParallelAnimation { - running: root.isClosing - onFinished: { - if (root.isClosing) { - root.isClosing = false; - } - } - - Anim { - target: parent - property: "opacity" - to: 0 - } - Anim { - target: parent - property: "scale" - to: 0.7 - } - } - - Keys.onEscapePressed: root.closeDialog() - - ColumnLayout { - id: content - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.normal - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "lock" - font.pointSize: Appearance.font.size.extraLarge * 2 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Enter password") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - - StyledText { - id: networkNameText - Layout.alignment: Qt.AlignHCenter - text: { - if (root.network) { - const ssid = root.network.ssid; - if (ssid && ssid.length > 0) { - return qsTr("Network: %1").arg(ssid); - } - } - return qsTr("Network: Unknown"); - } - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - Timer { - interval: 50 - running: root.shouldBeVisible && (!root.network || !root.network.ssid) - repeat: true - property int attempts: 0 - onTriggered: { - attempts++; - // Keep trying to get network from Network component - const content = root.parent?.parent?.parent; - if (content) { - const networkPopout = content.children.find(c => c.name === "network"); - if (networkPopout && networkPopout.item && networkPopout.item.passwordNetwork) { - root.network = networkPopout.item.passwordNetwork; - } - } - // Stop if we got it or after 20 attempts (1 second) - if ((root.network && root.network.ssid) || attempts >= 20) { - stop(); - attempts = 0; - } - } - onRunningChanged: { - if (!running) { - attempts = 0; - } - } - } - - StyledText { - id: statusText - - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.spacing.small - visible: connectButton.connecting || connectButton.hasError - text: { - if (connectButton.hasError) { - return qsTr("Connection failed. Please check your password and try again."); - } - if (connectButton.connecting) { - return qsTr("Connecting..."); - } - return ""; - } - color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small - font.weight: 400 - wrapMode: Text.WordWrap - Layout.maximumWidth: parent.width - Appearance.padding.large * 2 - } - - FocusScope { - id: passwordContainer - objectName: "passwordContainer" - Layout.topMargin: Appearance.spacing.large - Layout.fillWidth: true - implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) - - focus: true - activeFocusOnTab: true - - property string passwordBuffer: "" - - Keys.onPressed: event => { - // Ensure we have focus when receiving keyboard input - if (!activeFocus) { - forceActiveFocus(); - } - - // Clear error when user starts typing - if (connectButton.hasError && event.text && event.text.length > 0) { - connectButton.hasError = false; - } - - if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { - if (connectButton.enabled) { - connectButton.clicked(); - } - event.accepted = true; - } else if (event.key === Qt.Key_Backspace) { - if (event.modifiers & Qt.ControlModifier) { - passwordBuffer = ""; - } else { - passwordBuffer = passwordBuffer.slice(0, -1); - } - event.accepted = true; - } else if (event.text && event.text.length > 0) { - passwordBuffer += event.text; - event.accepted = true; - } - } - - Connections { - target: root - function onShouldBeVisibleChanged(): void { - if (root.shouldBeVisible) { - // Use Timer for actual delay to ensure focus works correctly - passwordFocusTimer.start(); - passwordContainer.passwordBuffer = ""; - connectButton.hasError = false; - } - } - } - - Timer { - id: passwordFocusTimer - interval: 50 - onTriggered: { - passwordContainer.forceActiveFocus(); - } - } - - Component.onCompleted: { - if (root.shouldBeVisible) { - // Use Timer for actual delay to ensure focus works correctly - passwordFocusTimer.start(); - } - } - - StyledRect { - anchors.fill: parent - radius: Appearance.rounding.normal - color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer - border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) - border.color: { - if (connectButton.hasError) { - return Colours.palette.m3error; - } - if (passwordContainer.activeFocus) { - return Colours.palette.m3primary; - } - return root.shouldBeVisible ? Colours.palette.m3outline : "transparent"; - } - - Behavior on border.color { - CAnim {} - } - - Behavior on border.width { - CAnim {} - } - - Behavior on color { - CAnim {} - } - } - - StateLayer { - hoverEnabled: false - cursorShape: Qt.IBeamCursor - radius: Appearance.rounding.normal - - function onClicked(): void { - passwordContainer.forceActiveFocus(); - } - } - - StyledText { - id: placeholder - - anchors.centerIn: parent - text: qsTr("Password") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono - opacity: passwordContainer.passwordBuffer ? 0 : 1 - - Behavior on opacity { - Anim {} - } - } - - ListView { - id: charList - - readonly property int fullWidth: count * (implicitHeight + spacing) - spacing - - anchors.centerIn: parent - implicitWidth: fullWidth - implicitHeight: Appearance.font.size.normal - - orientation: Qt.Horizontal - spacing: Appearance.spacing.small / 2 - interactive: false - - model: ScriptModel { - values: passwordContainer.passwordBuffer.split("") - } - - delegate: StyledRect { - id: ch - - implicitWidth: implicitHeight - implicitHeight: charList.implicitHeight - - color: Colours.palette.m3onSurface - radius: Appearance.rounding.small / 2 - - opacity: 0 - scale: 0 - Component.onCompleted: { - opacity = 1; - scale = 1; - } - ListView.onRemove: removeAnim.start() - - SequentialAnimation { - id: removeAnim - - PropertyAction { - target: ch - property: "ListView.delayRemove" - value: true - } - ParallelAnimation { - Anim { - target: ch - property: "opacity" - to: 0 - } - Anim { - target: ch - property: "scale" - to: 0.5 - } - } - PropertyAction { - target: ch - property: "ListView.delayRemove" - value: false - } - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } - } - - Behavior on implicitWidth { - Anim {} - } - } - } - - RowLayout { - Layout.topMargin: Appearance.spacing.normal - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - TextButton { - id: cancelButton - - Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 - inactiveColour: Colours.palette.m3secondaryContainer - inactiveOnColour: Colours.palette.m3onSecondaryContainer - text: qsTr("Cancel") - - onClicked: root.closeDialog() - } - - TextButton { - id: connectButton - - property bool connecting: false - property bool hasError: false - - Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 - inactiveColour: Colours.palette.m3primary - inactiveOnColour: Colours.palette.m3onPrimary - text: qsTr("Connect") - enabled: passwordContainer.passwordBuffer.length > 0 && !connecting - - onClicked: { - if (!root.network || connecting) { - return; - } - - const password = passwordContainer.passwordBuffer; - if (!password || password.length === 0) { - return; - } - - // Clear any previous error - hasError = false; - - // Set connecting state - connecting = true; - enabled = false; - text = qsTr("Connecting..."); - - // Connect to network - Nmcli.connectToNetwork(root.network.ssid, password, root.network.bssid || "", result => { - if (result && result.success) - // Connection successful, monitor will handle the rest - {} else if (result && result.needsPassword) { - // Shouldn't happen since we provided password - connectionMonitor.stop(); - connecting = false; - hasError = true; - enabled = true; - text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } else { - // Connection failed immediately - show error - connectionMonitor.stop(); - connecting = false; - hasError = true; - enabled = true; - text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } - }); - - // Start monitoring connection - connectionMonitor.start(); - } - } - } - } - } - - function checkConnectionStatus(): void { - if (!root.shouldBeVisible || !connectButton.connecting) { - return; - } - - // Check if we're connected to the target network (case-insensitive SSID comparison) - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - - if (isConnected) { - // Successfully connected - give it a moment for network list to update - // Use Timer for actual delay - connectionSuccessTimer.start(); - return; - } - - // Check for connection failures - if pending connection was cleared but we're not connected - if (Nmcli.pendingConnection === null && connectButton.connecting) { - // Wait a bit more before giving up (allow time for connection to establish) - if (connectionMonitor.repeatCount > 10) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } - } - } - - Timer { - id: connectionMonitor - interval: 1000 - repeat: true - triggeredOnStart: false - property int repeatCount: 0 - - onTriggered: { - repeatCount++; - root.checkConnectionStatus(); - } - - onRunningChanged: { - if (!running) { - repeatCount = 0; - } - } - } - - Timer { - id: connectionSuccessTimer - interval: 500 - onTriggered: { - // Double-check connection is still active - if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { - const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - if (stillConnected) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - // Return to network popout on successful connection - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; - } - closeDialog(); - } - } - } - } - - Connections { - target: Nmcli - function onActiveChanged() { - if (root.shouldBeVisible) { - root.checkConnectionStatus(); - } - } - function onConnectionFailed(ssid: string) { - if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - Nmcli.forgetNetwork(ssid); - } - } - } - - function closeDialog(): void { - if (isClosing) { - return; - } - - isClosing = true; - passwordContainer.passwordBuffer = ""; - connectButton.connecting = false; - connectButton.hasError = false; - connectButton.text = qsTr("Connect"); - connectionMonitor.stop(); - - // Return to network popout - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; - } - } -} -- cgit v1.2.3-freya From 1f719149daa7d5a1ec64f4c9dfe1ac44a66584cb Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 20:08:34 -0500 Subject: tray: wireless password popout respect transparency --- modules/bar/popouts/WirelessPassword.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index a9d098e..d91c87c 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -79,7 +79,8 @@ ColumnLayout { implicitHeight: content.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal - color: Colours.tPalette.m3surface + color: Colours.tPalette.m3surfaceContainer + visible: root.shouldBeVisible || root.isClosing opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 -- cgit v1.2.3-freya From 57bb5cede133be96f848fea9ffe5bc9deb1a6d91 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 21:40:47 -0500 Subject: controlcenter: lazyloading wallpapers FIXED --- .../controlcenter/appearance/AppearancePane.qml | 300 ++++++++++++++++++--- 1 file changed, 265 insertions(+), 35 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 2b3bbce..51e4cdf 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -50,42 +50,32 @@ RowLayout { function saveConfig() { - // Update animations Config.appearance.anim.durations.scale = root.animDurationsScale; - // Update fonts Config.appearance.font.family.material = root.fontFamilyMaterial; Config.appearance.font.family.mono = root.fontFamilyMono; Config.appearance.font.family.sans = root.fontFamilySans; Config.appearance.font.size.scale = root.fontSizeScale; - // Update scales Config.appearance.padding.scale = root.paddingScale; Config.appearance.rounding.scale = root.roundingScale; Config.appearance.spacing.scale = root.spacingScale; - // Update transparency Config.appearance.transparency.enabled = root.transparencyEnabled; Config.appearance.transparency.base = root.transparencyBase; Config.appearance.transparency.layers = root.transparencyLayers; - // Update desktop clock Config.background.desktopClock.enabled = root.desktopClockEnabled; - - // Update background enabled Config.background.enabled = root.backgroundEnabled; - // Update visualiser Config.background.visualiser.enabled = root.visualiserEnabled; Config.background.visualiser.autoHide = root.visualiserAutoHide; Config.background.visualiser.rounding = root.visualiserRounding; Config.background.visualiser.spacing = root.visualiserSpacing; - // Update border Config.border.rounding = root.borderRounding; Config.border.thickness = root.borderThickness; - // Persist changes to disk Config.save(); } @@ -224,13 +214,11 @@ RowLayout { function onClicked(): void { const variant = modelData.variant; - // Optimistic update - set immediately + // Optimistic update - set immediately for responsive UI Schemes.currentVariant = variant; - - // Execute the command Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); - // Reload after a delay to confirm + // Reload after a delay to confirm changes Qt.callLater(() => { reloadTimer.restart(); }); @@ -312,13 +300,11 @@ RowLayout { const flavour = modelData.flavour; const schemeKey = `${name} ${flavour}`; - // Optimistic update - set immediately + // Optimistic update - set immediately for responsive UI Schemes.currentScheme = schemeKey; - - // Execute the command Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - // Reload after a delay to confirm + // Reload after a delay to confirm changes Qt.callLater(() => { reloadTimer.restart(); }); @@ -1816,6 +1802,12 @@ RowLayout { asynchronous: true sourceComponent: appearanceRightContentComponent property var rootPane: root + + onStatusChanged: { + if (status === Loader.Error) { + console.error("[AppearancePane] Right appearance loader error!"); + } + } } } @@ -1884,25 +1876,99 @@ RowLayout { anchors.fill: parent asynchronous: true active: { - // Lazy load: only activate when right pane is loaded - // This defers heavy wallpaper list loading until the right pane is visible - return rightAppearanceLoader.item !== null; + // 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!"); + } } 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; + } + + // 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)) - readonly property int layoutPreferredHeight: Math.ceil(count / columnsCount) * cellHeight - height: layoutPreferredHeight + // 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; + } - // Distribute width evenly across columns + height: layoutPreferredHeight cellWidth: width / columnsCount cellHeight: 140 + Appearance.spacing.normal @@ -1911,13 +1977,178 @@ RowLayout { topMargin: 0 bottomMargin: 0 - model: Wallpapers.list + // 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]}); + } + } + }); + } - // Disable GridView's own scrolling - let parent handle it + Connections { + target: root.session + function onActiveIndexChanged(): void { + const isActive = root.session.activeIndex === 3; + 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 { + 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: lazyModel && lazyModel.sourceList && lazyModel.loadedCount < lazyModel.totalCount + repeat: true + onTriggered: { + 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 - // Enable caching for better performance - cacheBuffer: cellHeight * 2 delegate: Item { required property var modelData @@ -1928,6 +2159,10 @@ RowLayout { 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 @@ -1967,7 +2202,6 @@ RowLayout { antialiasing: true smooth: true - // Show when ready opacity: status === Image.Ready ? 1 : 0 Behavior on opacity { @@ -1978,7 +2212,7 @@ RowLayout { } } - // Fallback image for when caching fails + // Fallback if CachingImage fails to load Image { id: fallbackImage @@ -2001,7 +2235,6 @@ RowLayout { } } - // Timer to trigger fallback only if caching hasn't loaded Timer { id: fallbackTimer @@ -2011,7 +2244,7 @@ RowLayout { onTriggered: triggered = true } - // Gradient overlay for filename - positioned inside image container for perfect alignment + // Gradient overlay for filename Rectangle { id: filenameOverlay @@ -2020,8 +2253,6 @@ RowLayout { anchors.bottom: parent.bottom implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5 - - // No rounded corners - clipped by parent's rounded corners radius: 0 gradient: Gradient { @@ -2066,7 +2297,6 @@ RowLayout { } } - // Border overlay that doesn't affect image size Rectangle { anchors.fill: parent anchors.leftMargin: itemMargin -- cgit v1.2.3-freya From 9f8f8ac73145aacfa9aeaa6455210aac39579908 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 23:01:51 -0500 Subject: controlcenter: correcting containers (network details) --- modules/controlcenter/network/EthernetDetails.qml | 27 ++++++----------------- modules/controlcenter/network/NetworkingPane.qml | 27 +++++++++++++++++++---- modules/controlcenter/network/WirelessDetails.qml | 26 ++++++---------------- 3 files changed, 37 insertions(+), 43 deletions(-) diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index f715970..c16eb9d 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -16,6 +16,8 @@ Item { required property Session session readonly property var device: session.ethernet.active + anchors.fill: parent + Component.onCompleted: { if (device && device.interface) { Nmcli.getEthernetDeviceDetails(device.interface, () => {}); @@ -30,25 +32,12 @@ Item { } } - StyledFlickable { - id: flickable - - anchors.fill: parent - - flickableDirection: Flickable.VerticalFlick - clip: true - contentHeight: layout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: flickable - } - - ColumnLayout { - id: layout + ColumnLayout { + id: layout - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.normal + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal ConnectionHeader { icon: "cable" @@ -112,8 +101,6 @@ Item { deviceDetails: Nmcli.ethernetDeviceDetails } } - - } } } \ No newline at end of file diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 3601b8b..680e495 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -463,6 +463,7 @@ Item { opacity: 1 scale: 1 transformOrigin: Item.Center + clip: false asynchronous: true sourceComponent: pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings @@ -533,16 +534,34 @@ Item { Component { id: ethernetDetails - EthernetDetails { - session: root.session + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: ethernetDetailsInner.height + + EthernetDetails { + id: ethernetDetailsInner + + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } } } Component { id: wirelessDetails - WirelessDetails { - session: root.session + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: wirelessDetailsInner.height + + WirelessDetails { + id: wirelessDetailsInner + + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } } } } diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 82ac975..6b7cb15 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -17,6 +17,8 @@ Item { required property Session session readonly property var network: session.network.active + anchors.fill: parent + Component.onCompleted: { updateDeviceDetails(); checkSavedProfile(); @@ -98,25 +100,12 @@ Item { } } - StyledFlickable { - id: flickable - - anchors.fill: parent + ColumnLayout { + id: layout - flickableDirection: Flickable.VerticalFlick - clip: true - contentHeight: layout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: flickable - } - - ColumnLayout { - id: layout - - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.normal + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal ConnectionHeader { icon: root.network?.isSecure ? "lock" : "wifi" @@ -214,7 +203,6 @@ Item { deviceDetails: Nmcli.wirelessDeviceDetails } } - } } function handleConnect(): void { -- cgit v1.2.3-freya From ff0376067575ef61c39d5ee61745e43c315dd488 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 23:26:31 -0500 Subject: controlcenter: correcting containers (bluetooth pane) --- modules/controlcenter/bluetooth/Details.qml | 34 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 445f568..7ac5286 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -12,25 +12,29 @@ import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts -Item { +StyledFlickable { id: root required property Session session readonly property BluetoothDevice device: session.bt.active - StyledFlickable { - anchors.fill: parent + anchors.fill: parent - flickableDirection: Flickable.VerticalFlick - clip: true - contentHeight: layout.height + flickableDirection: Flickable.VerticalFlick + contentHeight: layoutWrapper.height - ColumnLayout { - id: layout + Item { + id: layoutWrapper anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + implicitHeight: layout.height + + ColumnLayout { + id: layout + + anchors.fill: parent + spacing: Appearance.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter @@ -416,8 +420,8 @@ Item { } } } + } } - } ColumnLayout { anchors.right: fabRoot.right @@ -563,11 +567,11 @@ Item { Item { id: fabRoot - anchors.right: parent.right - anchors.bottom: parent.bottom - - implicitWidth: 64 - implicitHeight: 64 + x: root.contentX + root.width - width + y: root.contentY + root.height - height + width: 64 + height: 64 + z: 10000 StyledRect { id: fabBg -- cgit v1.2.3-freya From 9bd3d57ff5cbeb883a71ebd9f4d9301afffbf4d9 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sun, 16 Nov 2025 23:33:54 -0500 Subject: controlcenter: lazyload cleanup confirmed --- .../controlcenter/appearance/AppearancePane.qml | 53 +++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 51e4cdf..b4e93ae 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -1891,6 +1891,20 @@ RowLayout { 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 @@ -1908,6 +1922,16 @@ RowLayout { 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 { @@ -2030,6 +2054,19 @@ RowLayout { 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(); @@ -2062,6 +2099,10 @@ RowLayout { 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; } @@ -2107,9 +2148,19 @@ RowLayout { Timer { id: scrollCheckTimer interval: 100 - running: lazyModel && lazyModel.sourceList && lazyModel.loadedCount < lazyModel.totalCount + 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; -- cgit v1.2.3-freya From c2c04b86379c222b5d0e0f123b12713e7a82366e Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 08:09:32 -0500 Subject: controlcenter: corrections on scrolling in panes and missing scrollbars --- modules/controlcenter/bluetooth/BtPane.qml | 7 +++++++ modules/controlcenter/bluetooth/Details.qml | 6 ++++-- modules/controlcenter/bluetooth/Settings.qml | 12 ++++++++---- modules/controlcenter/network/EthernetDetails.qml | 4 +++- modules/controlcenter/network/NetworkingPane.qml | 18 ++++++++++++++++++ modules/controlcenter/network/WirelessDetails.qml | 4 +++- 6 files changed, 43 insertions(+), 8 deletions(-) diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 32d2c0d..8ad4b1f 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import qs.components.controls import qs.components.effects import qs.components.containers import qs.config @@ -131,14 +132,20 @@ RowLayout { 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 } } diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 7ac5286..883b408 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -18,11 +18,13 @@ StyledFlickable { required property Session session readonly property BluetoothDevice device: session.bt.active - anchors.fill: parent - flickableDirection: Flickable.VerticalFlick contentHeight: layoutWrapper.height + StyledScrollBar.vertical: StyledScrollBar { + flickable: root + } + Item { id: layoutWrapper diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index fd33af9..c8453b6 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -284,8 +284,12 @@ ColumnLayout { CustomSpinBox { min: 0 - value: root.session.bt.currentAdapter.discoverableTimeout - onValueModified: value => root.session.bt.currentAdapter.discoverableTimeout = value + value: root.session.bt.currentAdapter?.discoverableTimeout ?? 0 + onValueModified: value => { + if (root.session.bt.currentAdapter) { + root.session.bt.currentAdapter.discoverableTimeout = value; + } + } } } @@ -345,7 +349,7 @@ ColumnLayout { anchors.top: renameLabel.bottom anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Appearance.padding.normal - text: root.session.bt.currentAdapter.name + text: root.session.bt.currentAdapter?.name ?? "" readOnly: !root.session.bt.editingAdapterName onAccepted: { root.session.bt.editingAdapterName = false; @@ -392,7 +396,7 @@ ColumnLayout { function onClicked(): void { root.session.bt.editingAdapterName = false; - adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter.name); + adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? ""); } } diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index c16eb9d..7c2534a 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -16,7 +16,8 @@ Item { required property Session session readonly property var device: session.ethernet.active - anchors.fill: parent + implicitWidth: layout.implicitWidth + implicitHeight: layout.implicitHeight Component.onCompleted: { if (device && device.interface) { @@ -37,6 +38,7 @@ Item { anchors.left: parent.left anchors.right: parent.right + anchors.top: parent.top spacing: Appearance.spacing.normal ConnectionHeader { diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 680e495..d00916c 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -518,14 +518,20 @@ Item { id: settings StyledFlickable { + id: settingsFlickable flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } + NetworkSettings { id: settingsInner anchors.left: parent.left anchors.right: parent.right + anchors.top: parent.top session: root.session } } @@ -535,14 +541,20 @@ Item { id: ethernetDetails StyledFlickable { + id: ethernetFlickable flickableDirection: Flickable.VerticalFlick contentHeight: ethernetDetailsInner.height + StyledScrollBar.vertical: StyledScrollBar { + flickable: ethernetFlickable + } + EthernetDetails { id: ethernetDetailsInner anchors.left: parent.left anchors.right: parent.right + anchors.top: parent.top session: root.session } } @@ -552,14 +564,20 @@ Item { id: wirelessDetails 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 } } diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 6b7cb15..09abff3 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -17,7 +17,8 @@ Item { required property Session session readonly property var network: session.network.active - anchors.fill: parent + implicitWidth: layout.implicitWidth + implicitHeight: layout.implicitHeight Component.onCompleted: { updateDeviceDetails(); @@ -105,6 +106,7 @@ Item { anchors.left: parent.left anchors.right: parent.right + anchors.top: parent.top spacing: Appearance.spacing.normal ConnectionHeader { -- cgit v1.2.3-freya From 29f7c7aa4f7e8c1964e46210bcf70f80614101ed Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 08:12:22 -0500 Subject: controlcenter: corrected scrolling in bluetooth details while selected --- modules/controlcenter/bluetooth/Details.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 883b408..c9d10cd 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -30,12 +30,15 @@ StyledFlickable { anchors.left: parent.left anchors.right: parent.right + anchors.top: parent.top implicitHeight: layout.height ColumnLayout { id: layout - anchors.fill: parent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top spacing: Appearance.spacing.normal MaterialIcon { -- cgit v1.2.3-freya From 8c84f670e6d47f2c07f51c9bdfdf77a0e2b89b77 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 08:13:44 -0500 Subject: controlcenter: corrected null anchor warn --- modules/controlcenter/bluetooth/DeviceList.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index 8bf5daa..db1656b 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -166,8 +166,8 @@ ColumnLayout { readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting readonly property bool connected: modelData.state === BluetoothDeviceState.Connected - anchors.left: parent.left - anchors.right: parent.right + anchors.left: view.contentItem.left + anchors.right: view.contentItem.right implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.bt.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) -- cgit v1.2.3-freya From 4a7c2dbdd5667f923677b3100f850ec19f117486 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 08:22:36 -0500 Subject: controlcenter: moved connection status/security type in networking --- modules/controlcenter/network/NetworkingPane.qml | 83 ++++++++++++++---------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index d00916c..93d3f93 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -215,19 +215,27 @@ Item { } } - StyledText { + ColumnLayout { Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - text: modelData.interface || qsTr("Unknown") - } + spacing: 0 + + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + + text: modelData.interface || qsTr("Unknown") + } - StyledText { - text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") - color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: modelData.connected ? 500 : 400 + StyledText { + Layout.fillWidth: true + text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.connected ? 500 : 400 + elide: Text.ElideRight + } } StyledRect { @@ -362,37 +370,46 @@ Item { } } - StyledText { + ColumnLayout { Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - text: (modelData && modelData.ssid) ? modelData.ssid : qsTr("Unknown") - } + spacing: 0 - RowLayout { - spacing: Appearance.spacing.smaller + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 - MaterialIcon { - visible: (modelData && modelData.isSecure) - text: "lock" - font.pointSize: Appearance.font.size.small - color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline + text: (modelData && modelData.ssid) ? modelData.ssid : qsTr("Unknown") } - StyledText { - text: { - if (!modelData) return qsTr("Open"); - if (modelData.active) return qsTr("Connected"); - if (modelData.isSecure && modelData.security && modelData.security.length > 0) { - return modelData.security; + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + MaterialIcon { + visible: (modelData && modelData.isSecure) + text: "lock" + font.pointSize: Appearance.font.size.small + color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline + } + + StyledText { + Layout.fillWidth: true + text: { + if (!modelData) return qsTr("Open"); + if (modelData.active) return qsTr("Connected"); + if (modelData.isSecure && modelData.security && modelData.security.length > 0) { + return modelData.security; + } + if (modelData.isSecure) return qsTr("Secured"); + return qsTr("Open"); } - if (modelData.isSecure) return qsTr("Secured"); - return qsTr("Open"); + color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: (modelData && modelData.active) ? 500 : 400 + elide: Text.ElideRight } - color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: (modelData && modelData.active) ? 500 : 400 } } -- cgit v1.2.3-freya From eb0286a282ff40b53aa9b43dd563f4c267c60274 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 08:36:14 -0500 Subject: controlcenter: lock badge on signal strength for secure networks --- modules/controlcenter/network/NetworkingPane.qml | 33 +++++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 93d3f93..dc568e2 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -368,6 +368,32 @@ Item { fill: (modelData && modelData.active) ? 1 : 0 color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } + + StyledRect { + id: lockBadge + + visible: modelData && modelData.isSecure + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: -Appearance.padding.smaller / 2 + + implicitWidth: lockIconSize + Appearance.padding.smaller + implicitHeight: lockIconSize + Appearance.padding.smaller + radius: Appearance.rounding.full + color: (modelData && modelData.active) ? Colours.palette.m3secondaryContainer : Colours.palette.m3surfaceContainerHighest + + readonly property real lockIconSize: lockIcon.implicitWidth + + MaterialIcon { + id: lockIcon + + anchors.centerIn: parent + text: "lock" + font.pointSize: Appearance.font.size.small + fill: 1 + color: (modelData && modelData.active) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + } } ColumnLayout { @@ -387,13 +413,6 @@ Item { Layout.fillWidth: true spacing: Appearance.spacing.smaller - MaterialIcon { - visible: (modelData && modelData.isSecure) - text: "lock" - font.pointSize: Appearance.font.size.small - color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline - } - StyledText { Layout.fillWidth: true text: { -- cgit v1.2.3-freya From 338b636de73606ca30494bf7f119dd77fc7bc9fd Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 08:38:25 -0500 Subject: controlcenter: corrected typeerror null checks --- modules/controlcenter/bluetooth/DeviceList.qml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index db1656b..8e79e72 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -163,8 +163,8 @@ ColumnLayout { id: device required property BluetoothDevice modelData - readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting - readonly property bool connected: modelData.state === BluetoothDeviceState.Connected + readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting) + readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected anchors.left: view.contentItem.left anchors.right: view.contentItem.right @@ -177,7 +177,8 @@ ColumnLayout { id: stateLayer function onClicked(): void { - root.session.bt.active = device.modelData; + if (device.modelData) + root.session.bt.active = device.modelData; } } @@ -194,20 +195,20 @@ ColumnLayout { implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal - color: device.connected ? Colours.palette.m3primaryContainer : device.modelData.bonded ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh + color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh StyledRect { anchors.fill: parent radius: parent.radius - color: Qt.alpha(device.connected ? Colours.palette.m3onPrimaryContainer : device.modelData.bonded ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) + color: Qt.alpha(device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) } MaterialIcon { id: icon anchors.centerIn: parent - text: Icons.getBluetoothIcon(device.modelData.icon) - color: device.connected ? Colours.palette.m3onPrimaryContainer : device.modelData.bonded ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : "") + color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface font.pointSize: Appearance.font.size.large fill: device.connected ? 1 : 0 @@ -224,13 +225,13 @@ ColumnLayout { StyledText { Layout.fillWidth: true - text: device.modelData.name + text: device.modelData ? device.modelData.name : qsTr("Unknown") elide: Text.ElideRight } StyledText { Layout.fillWidth: true - text: device.modelData.address + (device.connected ? qsTr(" (Connected)") : device.modelData.bonded ? qsTr(" (Paired)") : "") + text: (device.modelData ? device.modelData.address : "") + (device.connected ? qsTr(" (Connected)") : (device.modelData && device.modelData.bonded) ? qsTr(" (Paired)") : "") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small elide: Text.ElideRight @@ -256,7 +257,8 @@ ColumnLayout { disabled: device.loading function onClicked(): void { - device.modelData.connected = !device.modelData.connected; + if (device.modelData) + device.modelData.connected = !device.modelData.connected; } } @@ -265,7 +267,7 @@ ColumnLayout { anchors.centerIn: parent animate: true - text: device.modelData.connected ? "link_off" : "link" + text: (device.modelData && device.modelData.connected) ? "link_off" : "link" color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface opacity: device.loading ? 0 : 1 -- cgit v1.2.3-freya From f24805190444c1325e25829b7b4921c79742d6a3 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 08:41:42 -0500 Subject: controlcenter: taskbar clock settings bg correction --- modules/controlcenter/taskbar/TaskbarPane.qml | 29 ++++++--------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index c731acc..8a58512 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -213,30 +213,13 @@ RowLayout { CollapsibleSection { id: clockSection title: qsTr("Clock") - description: qsTr("Clock display settings") - RowLayout { - id: clockRow - - Layout.fillWidth: true - Layout.leftMargin: Appearance.padding.large - Layout.rightMargin: Appearance.padding.large - Layout.alignment: Qt.AlignVCenter - - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show clock icon") - } - - StyledSwitch { - id: clockShowIconSwitch - checked: root.clockShowIcon - onToggled: { - root.clockShowIcon = checked; - root.saveConfig(); - } + SwitchRow { + label: qsTr("Show clock icon") + checked: root.clockShowIcon + onToggled: checked => { + root.clockShowIcon = checked; + root.saveConfig(); } } } -- cgit v1.2.3-freya From 2c91bde79573b45f65280d7b68cd28f50a780019 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 09:24:28 -0500 Subject: controlcenter: converted taskbar panel to single pane view --- modules/controlcenter/taskbar/TaskbarPane.qml | 854 +++++++++++--------------- 1 file changed, 370 insertions(+), 484 deletions(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 8a58512..1ad35b6 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -13,7 +13,7 @@ import Quickshell.Widgets import QtQuick import QtQuick.Layouts -RowLayout { +Item { id: root required property Session session @@ -49,8 +49,6 @@ RowLayout { anchors.fill: parent - spacing: 0 - Component.onCompleted: { // Update entries if (Config.bar.entries) { @@ -120,604 +118,492 @@ RowLayout { id: entriesModel } - Item { - id: leftTaskbarItem - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - ClippingRectangle { - id: leftTaskbarClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 + ClippingRectangle { + id: taskbarClippingRect + anchors.fill: parent + anchors.margins: Appearance.padding.normal - radius: leftTaskbarBorder.innerRadius - color: "transparent" + radius: taskbarBorder.innerRadius + color: "transparent" - Loader { - id: leftTaskbarLoader + Loader { + id: taskbarLoader - 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 + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 - asynchronous: true - sourceComponent: leftTaskbarContentComponent - } + asynchronous: true + sourceComponent: taskbarContentComponent } + } - InnerBorder { - id: leftTaskbarBorder - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } + InnerBorder { + id: taskbarBorder + } - Component { - id: leftTaskbarContentComponent + Component { + id: taskbarContentComponent - StyledFlickable { - id: sidebarFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: sidebarLayout.height + StyledFlickable { + id: sidebarFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: sidebarLayout.height - StyledScrollBar.vertical: StyledScrollBar { - flickable: sidebarFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: sidebarFlickable + } - ColumnLayout { - id: sidebarLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top + ColumnLayout { + id: sidebarLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top - spacing: Appearance.spacing.small + spacing: Appearance.spacing.small - readonly property bool allSectionsExpanded: - clockSection.expanded && - barBehaviorSection.expanded && - statusIconsSection.expanded && - traySettingsSection.expanded && - workspacesSection.expanded + readonly property bool allSectionsExpanded: + clockSection.expanded && + barBehaviorSection.expanded && + statusIconsSection.expanded && + traySettingsSection.expanded && + workspacesSection.expanded - RowLayout { - spacing: Appearance.spacing.smaller + RowLayout { + spacing: Appearance.spacing.smaller - StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - IconButton { - icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more" - type: IconButton.Text - label.animate: true - onClicked: { - const shouldExpand = !sidebarLayout.allSectionsExpanded; - clockSection.expanded = shouldExpand; - barBehaviorSection.expanded = shouldExpand; - statusIconsSection.expanded = shouldExpand; - traySettingsSection.expanded = shouldExpand; - workspacesSection.expanded = shouldExpand; - } + IconButton { + icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more" + type: IconButton.Text + label.animate: true + onClicked: { + const shouldExpand = !sidebarLayout.allSectionsExpanded; + clockSection.expanded = shouldExpand; + barBehaviorSection.expanded = shouldExpand; + statusIconsSection.expanded = shouldExpand; + traySettingsSection.expanded = shouldExpand; + workspacesSection.expanded = shouldExpand; } } + } - CollapsibleSection { - id: clockSection - title: qsTr("Clock") + CollapsibleSection { + id: clockSection + title: qsTr("Clock") - SwitchRow { - label: qsTr("Show clock icon") - checked: root.clockShowIcon - onToggled: checked => { - root.clockShowIcon = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Show clock icon") + checked: root.clockShowIcon + onToggled: checked => { + root.clockShowIcon = checked; + root.saveConfig(); + } } } - } - CollapsibleSection { - id: barBehaviorSection - title: qsTr("Bar Behavior") + CollapsibleSection { + id: barBehaviorSection + title: qsTr("Bar Behavior") - SwitchRow { - label: qsTr("Persistent") - checked: root.persistent - onToggled: checked => { - root.persistent = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Persistent") + checked: root.persistent + onToggled: checked => { + root.persistent = checked; + root.saveConfig(); + } } - } - SwitchRow { - label: qsTr("Show on hover") - checked: root.showOnHover - onToggled: checked => { - root.showOnHover = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Show on hover") + checked: root.showOnHover + onToggled: checked => { + root.showOnHover = checked; + root.saveConfig(); + } } - } - SectionContainer { - contentSpacing: Appearance.spacing.normal + SectionContainer { + contentSpacing: Appearance.spacing.normal - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { + ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.small - StyledText { - text: qsTr("Drag threshold") - font.pointSize: Appearance.font.size.normal - } - - Item { + RowLayout { Layout.fillWidth: true - } + spacing: Appearance.spacing.normal - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: dragThresholdInput.implicitHeight + Appearance.padding.small * 2 - color: dragThresholdInputHover.containsMouse || dragThresholdInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: dragThresholdInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: dragThresholdInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton + StyledText { + text: qsTr("Drag threshold") + font.pointSize: Appearance.font.size.normal } - StyledTextField { - id: dragThresholdInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } - - Component.onCompleted: { - text = root.dragThreshold.toString(); + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: dragThresholdInput.implicitHeight + Appearance.padding.small * 2 + color: dragThresholdInputHover.containsMouse || dragThresholdInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: dragThresholdInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: dragThresholdInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton } - - onTextChanged: { - if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - root.dragThreshold = val; - root.saveConfig(); + + StyledTextField { + id: dragThresholdInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 100 } + + Component.onCompleted: { + text = root.dragThreshold.toString(); + } + + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + root.dragThreshold = val; + root.saveConfig(); + } } } - } - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = root.dragThreshold.toString(); + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = root.dragThreshold.toString(); + } } } } - } - StyledText { - text: "px" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal + StyledText { + text: "px" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } } - } - StyledSlider { - id: dragThresholdSlider + StyledSlider { + id: dragThresholdSlider - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0 - to: 100 - value: root.dragThreshold - onMoved: { - root.dragThreshold = Math.round(dragThresholdSlider.value); - if (!dragThresholdInput.activeFocus) { - dragThresholdInput.text = Math.round(dragThresholdSlider.value).toString(); + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0 + to: 100 + value: root.dragThreshold + onMoved: { + root.dragThreshold = Math.round(dragThresholdSlider.value); + if (!dragThresholdInput.activeFocus) { + dragThresholdInput.text = Math.round(dragThresholdSlider.value).toString(); + } + root.saveConfig(); } - root.saveConfig(); } } } } - } - CollapsibleSection { - id: statusIconsSection - title: qsTr("Status Icons") + CollapsibleSection { + id: statusIconsSection + title: qsTr("Status Icons") - SwitchRow { - label: qsTr("Show audio") - checked: root.showAudio - onToggled: checked => { - root.showAudio = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Show audio") + checked: root.showAudio + onToggled: checked => { + root.showAudio = checked; + root.saveConfig(); + } } - } - SwitchRow { - label: qsTr("Show microphone") - checked: root.showMicrophone - onToggled: checked => { - root.showMicrophone = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Show microphone") + checked: root.showMicrophone + onToggled: checked => { + root.showMicrophone = checked; + root.saveConfig(); + } } - } - SwitchRow { - label: qsTr("Show keyboard layout") - checked: root.showKbLayout - onToggled: checked => { - root.showKbLayout = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Show keyboard layout") + checked: root.showKbLayout + onToggled: checked => { + root.showKbLayout = checked; + root.saveConfig(); + } } - } - SwitchRow { - label: qsTr("Show network") - checked: root.showNetwork - onToggled: checked => { - root.showNetwork = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Show network") + checked: root.showNetwork + onToggled: checked => { + root.showNetwork = checked; + root.saveConfig(); + } } - } - SwitchRow { - label: qsTr("Show bluetooth") - checked: root.showBluetooth - onToggled: checked => { - root.showBluetooth = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Show bluetooth") + checked: root.showBluetooth + onToggled: checked => { + root.showBluetooth = checked; + root.saveConfig(); + } } - } - SwitchRow { - label: qsTr("Show battery") - checked: root.showBattery - onToggled: checked => { - root.showBattery = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Show battery") + checked: root.showBattery + onToggled: checked => { + root.showBattery = checked; + root.saveConfig(); + } } - } - SwitchRow { - label: qsTr("Show lock status") - checked: root.showLockStatus - onToggled: checked => { - root.showLockStatus = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Show lock status") + checked: root.showLockStatus + onToggled: checked => { + root.showLockStatus = checked; + root.saveConfig(); + } } } - } - CollapsibleSection { - id: traySettingsSection - title: qsTr("Tray Settings") + CollapsibleSection { + id: traySettingsSection + title: qsTr("Tray Settings") - SwitchRow { - label: qsTr("Background") - checked: root.trayBackground - onToggled: checked => { - root.trayBackground = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Background") + checked: root.trayBackground + onToggled: checked => { + root.trayBackground = checked; + root.saveConfig(); + } } - } - SwitchRow { - label: qsTr("Compact") - checked: root.trayCompact - onToggled: checked => { - root.trayCompact = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Compact") + checked: root.trayCompact + onToggled: checked => { + root.trayCompact = checked; + root.saveConfig(); + } } - } - SwitchRow { - label: qsTr("Recolour") - checked: root.trayRecolour - onToggled: checked => { - root.trayRecolour = checked; - root.saveConfig(); + SwitchRow { + label: qsTr("Recolour") + checked: root.trayRecolour + onToggled: checked => { + root.trayRecolour = checked; + root.saveConfig(); + } } } - } - CollapsibleSection { - id: workspacesSection - title: qsTr("Workspaces") + CollapsibleSection { + id: workspacesSection + title: qsTr("Workspaces") - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - Behavior on implicitHeight { - Anim {} - } + Behavior on implicitHeight { + Anim {} + } - RowLayout { - id: workspacesShownRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + RowLayout { + id: workspacesShownRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - StyledText { - Layout.fillWidth: true - text: qsTr("Shown") - } + StyledText { + Layout.fillWidth: true + text: qsTr("Shown") + } - CustomSpinBox { - min: 1 - max: 20 - value: root.workspacesShown - onValueModified: value => { - root.workspacesShown = value; - root.saveConfig(); + CustomSpinBox { + min: 1 + max: 20 + value: root.workspacesShown + onValueModified: value => { + root.workspacesShown = value; + root.saveConfig(); + } } } } - } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - Behavior on implicitHeight { - Anim {} - } + Behavior on implicitHeight { + Anim {} + } - RowLayout { - id: workspacesActiveIndicatorRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + RowLayout { + id: workspacesActiveIndicatorRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - StyledText { - Layout.fillWidth: true - text: qsTr("Active indicator") - } + StyledText { + Layout.fillWidth: true + text: qsTr("Active indicator") + } - StyledSwitch { - checked: root.workspacesActiveIndicator - onToggled: { - root.workspacesActiveIndicator = checked; - root.saveConfig(); + StyledSwitch { + checked: root.workspacesActiveIndicator + onToggled: { + root.workspacesActiveIndicator = checked; + root.saveConfig(); + } } } } - } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - Behavior on implicitHeight { - Anim {} - } + Behavior on implicitHeight { + Anim {} + } - RowLayout { - id: workspacesOccupiedBgRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + RowLayout { + id: workspacesOccupiedBgRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - StyledText { - Layout.fillWidth: true - text: qsTr("Occupied background") - } + StyledText { + Layout.fillWidth: true + text: qsTr("Occupied background") + } - StyledSwitch { - checked: root.workspacesOccupiedBg - onToggled: { - root.workspacesOccupiedBg = checked; - root.saveConfig(); + StyledSwitch { + checked: root.workspacesOccupiedBg + onToggled: { + root.workspacesOccupiedBg = checked; + root.saveConfig(); + } } } } - } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - Behavior on implicitHeight { - Anim {} - } + Behavior on implicitHeight { + Anim {} + } - RowLayout { - id: workspacesShowWindowsRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + RowLayout { + id: workspacesShowWindowsRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - StyledText { - Layout.fillWidth: true - text: qsTr("Show windows") - } + StyledText { + Layout.fillWidth: true + text: qsTr("Show windows") + } - StyledSwitch { - checked: root.workspacesShowWindows - onToggled: { - root.workspacesShowWindows = checked; - root.saveConfig(); + StyledSwitch { + checked: root.workspacesShowWindows + onToggled: { + root.workspacesShowWindows = checked; + root.saveConfig(); + } } } } - } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - Behavior on implicitHeight { - Anim {} - } + Behavior on implicitHeight { + Anim {} + } - RowLayout { - id: workspacesPerMonitorRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + RowLayout { + id: workspacesPerMonitorRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - StyledText { - Layout.fillWidth: true - text: qsTr("Per monitor workspaces") - } + StyledText { + Layout.fillWidth: true + text: qsTr("Per monitor workspaces") + } - StyledSwitch { - checked: root.workspacesPerMonitor - onToggled: { - root.workspacesPerMonitor = checked; - root.saveConfig(); + StyledSwitch { + checked: root.workspacesPerMonitor + onToggled: { + root.workspacesPerMonitor = checked; + root.saveConfig(); + } } } } } } } - } - } - } - - Item { - id: rightTaskbarItem - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - id: rightTaskbarClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: rightTaskbarBorder.innerRadius - color: "transparent" - - Loader { - id: rightTaskbarLoader - - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - - asynchronous: true - sourceComponent: rightTaskbarContentComponent - } - } - - InnerBorder { - id: rightTaskbarBorder - - leftThickness: Appearance.padding.normal / 2 - } - - Component { - id: rightTaskbarContentComponent - - StyledFlickable { - flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: parent - } - - ColumnLayout { - id: contentLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - - spacing: Appearance.spacing.normal - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "task_alt" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Taskbar Settings") - font.pointSize: Appearance.font.size.large - font.bold: true - } - - StyledText { - Layout.topMargin: Appearance.spacing.large - Layout.alignment: Qt.AlignHCenter - text: qsTr("Clock") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: root.clockShowIcon ? qsTr("Clock icon enabled") : qsTr("Clock icon disabled") - color: Colours.palette.m3outline - } - - StyledText { - Layout.topMargin: Appearance.spacing.large - Layout.alignment: Qt.AlignHCenter - text: qsTr("Taskbar Entries") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Configure which entries appear in the taskbar") - color: Colours.palette.m3outline - } - - } - } - } } } -- cgit v1.2.3-freya From 3f3f3a7d788ac555a94446731749e7b3aa9319c6 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 10:23:32 -0500 Subject: controlcenter: connected button group component --- .../controlcenter/taskbar/ConnectedButtonGroup.qml | 141 +++++++++++++++++++++ modules/controlcenter/taskbar/TaskbarPane.qml | 122 +++++++++--------- 2 files changed, 202 insertions(+), 61 deletions(-) create mode 100644 modules/controlcenter/taskbar/ConnectedButtonGroup.qml diff --git a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml b/modules/controlcenter/taskbar/ConnectedButtonGroup.qml new file mode 100644 index 0000000..7999b70 --- /dev/null +++ b/modules/controlcenter/taskbar/ConnectedButtonGroup.qml @@ -0,0 +1,141 @@ +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + property var options: [] // Array of {label: string, propertyName: string, onToggled: function} + property var rootItem: null // The root item that contains the properties we want to bind to + + Layout.fillWidth: true + implicitHeight: buttonRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + clip: true + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: buttonRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.small + + Repeater { + id: repeater + model: root.options + + delegate: TextButton { + id: button + required property int index + required property var modelData + + Layout.fillWidth: true + text: modelData.label + + property bool isChecked: false + + // Initialize from root property + Component.onCompleted: { + if (root.rootItem && modelData.propertyName) { + isChecked = root.rootItem[modelData.propertyName]; + } + } + + checked: isChecked + toggle: false + type: TextButton.Tonal + + // Listen for property changes on rootItem + Connections { + target: root.rootItem + enabled: root.rootItem !== null && modelData.propertyName !== undefined + + function onShowAudioChanged() { + if (modelData.propertyName === "showAudio") { + button.isChecked = root.rootItem.showAudio; + } + } + + function onShowMicrophoneChanged() { + if (modelData.propertyName === "showMicrophone") { + button.isChecked = root.rootItem.showMicrophone; + } + } + + function onShowKbLayoutChanged() { + if (modelData.propertyName === "showKbLayout") { + button.isChecked = root.rootItem.showKbLayout; + } + } + + function onShowNetworkChanged() { + if (modelData.propertyName === "showNetwork") { + button.isChecked = root.rootItem.showNetwork; + } + } + + function onShowBluetoothChanged() { + if (modelData.propertyName === "showBluetooth") { + button.isChecked = root.rootItem.showBluetooth; + } + } + + function onShowBatteryChanged() { + if (modelData.propertyName === "showBattery") { + button.isChecked = root.rootItem.showBattery; + } + } + + function onShowLockStatusChanged() { + if (modelData.propertyName === "showLockStatus") { + button.isChecked = root.rootItem.showLockStatus; + } + } + } + + + // Match utilities Toggles radius styling + // Each button has full rounding (not connected) since they have spacing + radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal + + // Match utilities Toggles inactive color + inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + + // Adjust width similar to utilities toggles + Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) + + onClicked: { + if (modelData.onToggled) { + modelData.onToggled(!checked); + } + } + + Behavior on Layout.preferredWidth { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + Behavior on radius { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } + } + } +} + diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 1ad35b6..d7cc664 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -336,67 +336,67 @@ Item { id: statusIconsSection title: qsTr("Status Icons") - SwitchRow { - label: qsTr("Show audio") - checked: root.showAudio - onToggled: checked => { - root.showAudio = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Show microphone") - checked: root.showMicrophone - onToggled: checked => { - root.showMicrophone = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Show keyboard layout") - checked: root.showKbLayout - onToggled: checked => { - root.showKbLayout = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Show network") - checked: root.showNetwork - onToggled: checked => { - root.showNetwork = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Show bluetooth") - checked: root.showBluetooth - onToggled: checked => { - root.showBluetooth = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Show battery") - checked: root.showBattery - onToggled: checked => { - root.showBattery = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Show lock status") - checked: root.showLockStatus - onToggled: checked => { - root.showLockStatus = checked; - root.saveConfig(); - } + ConnectedButtonGroup { + rootItem: root + + options: [ + { + label: qsTr("Audio"), + propertyName: "showAudio", + onToggled: function(checked) { + root.showAudio = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Mic"), + propertyName: "showMicrophone", + onToggled: function(checked) { + root.showMicrophone = checked; + root.saveConfig(); + } + }, + { + label: qsTr("KB"), + propertyName: "showKbLayout", + onToggled: function(checked) { + root.showKbLayout = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Network"), + propertyName: "showNetwork", + onToggled: function(checked) { + root.showNetwork = checked; + root.saveConfig(); + } + }, + { + label: qsTr("BT"), + propertyName: "showBluetooth", + onToggled: function(checked) { + root.showBluetooth = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Battery"), + propertyName: "showBattery", + onToggled: function(checked) { + root.showBattery = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Lock"), + propertyName: "showLockStatus", + onToggled: function(checked) { + root.showLockStatus = checked; + root.saveConfig(); + } + } + ] } } -- cgit v1.2.3-freya From 388cb0e373b1615b6d90028d74094c222fa48460 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 10:36:17 -0500 Subject: controlcenter: refined connected button groups --- .../controlcenter/taskbar/ConnectedButtonGroup.qml | 185 +++++++++++---------- modules/controlcenter/taskbar/TaskbarPane.qml | 134 +++++++-------- 2 files changed, 162 insertions(+), 157 deletions(-) diff --git a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml b/modules/controlcenter/taskbar/ConnectedButtonGroup.qml index 7999b70..83ff95e 100644 --- a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml +++ b/modules/controlcenter/taskbar/ConnectedButtonGroup.qml @@ -12,9 +12,10 @@ StyledRect { property var options: [] // Array of {label: string, propertyName: string, onToggled: function} property var rootItem: null // The root item that contains the properties we want to bind to + property string title: "" // Optional title text Layout.fillWidth: true - implicitHeight: buttonRow.implicitHeight + Appearance.padding.large * 2 + implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) clip: true @@ -23,115 +24,125 @@ StyledRect { Anim {} } - RowLayout { - id: buttonRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + ColumnLayout { + id: layout + + anchors.fill: parent anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.small - - Repeater { - id: repeater - model: root.options - - delegate: TextButton { - id: button - required property int index - required property var modelData - - Layout.fillWidth: true - text: modelData.label - - property bool isChecked: false - - // Initialize from root property - Component.onCompleted: { - if (root.rootItem && modelData.propertyName) { - isChecked = root.rootItem[modelData.propertyName]; - } - } - - checked: isChecked - toggle: false - type: TextButton.Tonal - - // Listen for property changes on rootItem - Connections { - target: root.rootItem - enabled: root.rootItem !== null && modelData.propertyName !== undefined - - function onShowAudioChanged() { - if (modelData.propertyName === "showAudio") { - button.isChecked = root.rootItem.showAudio; + spacing: Appearance.spacing.normal + + StyledText { + visible: root.title !== "" + text: root.title + font.pointSize: Appearance.font.size.normal + } + + RowLayout { + id: buttonRow + Layout.alignment: Qt.AlignHCenter + spacing: Appearance.spacing.small + + Repeater { + id: repeater + model: root.options + + delegate: TextButton { + id: button + required property int index + required property var modelData + + Layout.fillWidth: true + text: modelData.label + + property bool isChecked: false + + // Initialize from root property + Component.onCompleted: { + if (root.rootItem && modelData.propertyName) { + isChecked = root.rootItem[modelData.propertyName]; } } + + checked: isChecked + toggle: false + type: TextButton.Tonal + + // Listen for property changes on rootItem + Connections { + target: root.rootItem + enabled: root.rootItem !== null && modelData.propertyName !== undefined + + function onShowAudioChanged() { + if (modelData.propertyName === "showAudio") { + button.isChecked = root.rootItem.showAudio; + } + } - function onShowMicrophoneChanged() { - if (modelData.propertyName === "showMicrophone") { - button.isChecked = root.rootItem.showMicrophone; + function onShowMicrophoneChanged() { + if (modelData.propertyName === "showMicrophone") { + button.isChecked = root.rootItem.showMicrophone; + } } - } - function onShowKbLayoutChanged() { - if (modelData.propertyName === "showKbLayout") { - button.isChecked = root.rootItem.showKbLayout; + function onShowKbLayoutChanged() { + if (modelData.propertyName === "showKbLayout") { + button.isChecked = root.rootItem.showKbLayout; + } } - } - function onShowNetworkChanged() { - if (modelData.propertyName === "showNetwork") { - button.isChecked = root.rootItem.showNetwork; + function onShowNetworkChanged() { + if (modelData.propertyName === "showNetwork") { + button.isChecked = root.rootItem.showNetwork; + } } - } - function onShowBluetoothChanged() { - if (modelData.propertyName === "showBluetooth") { - button.isChecked = root.rootItem.showBluetooth; + function onShowBluetoothChanged() { + if (modelData.propertyName === "showBluetooth") { + button.isChecked = root.rootItem.showBluetooth; + } } - } - function onShowBatteryChanged() { - if (modelData.propertyName === "showBattery") { - button.isChecked = root.rootItem.showBattery; + function onShowBatteryChanged() { + if (modelData.propertyName === "showBattery") { + button.isChecked = root.rootItem.showBattery; + } } - } - function onShowLockStatusChanged() { - if (modelData.propertyName === "showLockStatus") { - button.isChecked = root.rootItem.showLockStatus; + function onShowLockStatusChanged() { + if (modelData.propertyName === "showLockStatus") { + button.isChecked = root.rootItem.showLockStatus; + } } } - } + // Match utilities Toggles radius styling + // Each button has full rounding (not connected) since they have spacing + radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal - // Match utilities Toggles radius styling - // Each button has full rounding (not connected) since they have spacing - radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal + // Match utilities Toggles inactive color + inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + + // Adjust width similar to utilities toggles + Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) - // Match utilities Toggles inactive color - inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) - - // Adjust width similar to utilities toggles - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) - - onClicked: { - if (modelData.onToggled) { - modelData.onToggled(!checked); + onClicked: { + if (modelData.onToggled) { + modelData.onToggled(!checked); + } } - } - Behavior on Layout.preferredWidth { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + Behavior on Layout.preferredWidth { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } } - } - Behavior on radius { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + Behavior on radius { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } } } } diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index d7cc664..0ec27e5 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -164,7 +164,6 @@ Item { readonly property bool allSectionsExpanded: clockSection.expanded && barBehaviorSection.expanded && - statusIconsSection.expanded && traySettingsSection.expanded && workspacesSection.expanded @@ -189,13 +188,76 @@ Item { const shouldExpand = !sidebarLayout.allSectionsExpanded; clockSection.expanded = shouldExpand; barBehaviorSection.expanded = shouldExpand; - statusIconsSection.expanded = shouldExpand; traySettingsSection.expanded = shouldExpand; workspacesSection.expanded = shouldExpand; } } } + ConnectedButtonGroup { + rootItem: root + title: qsTr("Status Icons") + + options: [ + { + label: qsTr("Audio"), + propertyName: "showAudio", + onToggled: function(checked) { + root.showAudio = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Mic"), + propertyName: "showMicrophone", + onToggled: function(checked) { + root.showMicrophone = checked; + root.saveConfig(); + } + }, + { + label: qsTr("KB"), + propertyName: "showKbLayout", + onToggled: function(checked) { + root.showKbLayout = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Network"), + propertyName: "showNetwork", + onToggled: function(checked) { + root.showNetwork = checked; + root.saveConfig(); + } + }, + { + label: qsTr("BT"), + propertyName: "showBluetooth", + onToggled: function(checked) { + root.showBluetooth = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Battery"), + propertyName: "showBattery", + onToggled: function(checked) { + root.showBattery = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Lock"), + propertyName: "showLockStatus", + onToggled: function(checked) { + root.showLockStatus = checked; + root.saveConfig(); + } + } + ] + } + CollapsibleSection { id: clockSection title: qsTr("Clock") @@ -332,74 +394,6 @@ Item { } } - CollapsibleSection { - id: statusIconsSection - title: qsTr("Status Icons") - - ConnectedButtonGroup { - rootItem: root - - options: [ - { - label: qsTr("Audio"), - propertyName: "showAudio", - onToggled: function(checked) { - root.showAudio = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Mic"), - propertyName: "showMicrophone", - onToggled: function(checked) { - root.showMicrophone = checked; - root.saveConfig(); - } - }, - { - label: qsTr("KB"), - propertyName: "showKbLayout", - onToggled: function(checked) { - root.showKbLayout = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Network"), - propertyName: "showNetwork", - onToggled: function(checked) { - root.showNetwork = checked; - root.saveConfig(); - } - }, - { - label: qsTr("BT"), - propertyName: "showBluetooth", - onToggled: function(checked) { - root.showBluetooth = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Battery"), - propertyName: "showBattery", - onToggled: function(checked) { - root.showBattery = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Lock"), - propertyName: "showLockStatus", - onToggled: function(checked) { - root.showLockStatus = checked; - root.saveConfig(); - } - } - ] - } - } - CollapsibleSection { id: traySettingsSection title: qsTr("Tray Settings") -- cgit v1.2.3-freya From 1850a844eef715147fa747209fe4d5d1e7cbd7a5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 10:43:20 -0500 Subject: controlcenter: relabled buttons in connected group --- modules/controlcenter/taskbar/TaskbarPane.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 0ec27e5..915e611 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -208,7 +208,7 @@ Item { } }, { - label: qsTr("Mic"), + label: qsTr("Microphone"), propertyName: "showMicrophone", onToggled: function(checked) { root.showMicrophone = checked; @@ -216,7 +216,7 @@ Item { } }, { - label: qsTr("KB"), + label: qsTr("Keyboard"), propertyName: "showKbLayout", onToggled: function(checked) { root.showKbLayout = checked; @@ -232,7 +232,7 @@ Item { } }, { - label: qsTr("BT"), + label: qsTr("Bluetooth"), propertyName: "showBluetooth", onToggled: function(checked) { root.showBluetooth = checked; @@ -248,7 +248,7 @@ Item { } }, { - label: qsTr("Lock"), + label: qsTr("Capslock"), propertyName: "showLockStatus", onToggled: function(checked) { root.showLockStatus = checked; -- cgit v1.2.3-freya From 821ee4389ab2a81c5abba910936662d22e5f0480 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 12:21:44 -0500 Subject: controlcenter: taskbar panel layout reorg --- components/SectionContainer.qml | 4 +- .../controlcenter/taskbar/ConnectedButtonGroup.qml | 18 + modules/controlcenter/taskbar/TaskbarPane.qml | 578 +++++++++++---------- 3 files changed, 328 insertions(+), 272 deletions(-) diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml index f7dfef4..60e3d59 100644 --- a/components/SectionContainer.qml +++ b/components/SectionContainer.qml @@ -10,6 +10,7 @@ StyledRect { default property alias content: contentColumn.data property real contentSpacing: Appearance.spacing.larger + property bool alignTop: false Layout.fillWidth: true implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2 @@ -22,7 +23,8 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + anchors.top: root.alignTop ? parent.top : undefined + anchors.verticalCenter: root.alignTop ? undefined : parent.verticalCenter anchors.margins: Appearance.padding.large spacing: root.contentSpacing diff --git a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml b/modules/controlcenter/taskbar/ConnectedButtonGroup.qml index 83ff95e..e35ccfc 100644 --- a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml +++ b/modules/controlcenter/taskbar/ConnectedButtonGroup.qml @@ -113,6 +113,24 @@ StyledRect { button.isChecked = root.rootItem.showLockStatus; } } + + function onTrayBackgroundChanged() { + if (modelData.propertyName === "trayBackground") { + button.isChecked = root.rootItem.trayBackground; + } + } + + function onTrayCompactChanged() { + if (modelData.propertyName === "trayCompact") { + button.isChecked = root.rootItem.trayCompact; + } + } + + function onTrayRecolourChanged() { + if (modelData.propertyName === "trayRecolour") { + button.isChecked = root.rootItem.trayRecolour; + } + } } // Match utilities Toggles radius styling diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 915e611..3f14dd6 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -161,11 +161,7 @@ Item { spacing: Appearance.spacing.small - readonly property bool allSectionsExpanded: - clockSection.expanded && - barBehaviorSection.expanded && - traySettingsSection.expanded && - workspacesSection.expanded + readonly property bool allSectionsExpanded: true RowLayout { spacing: Appearance.spacing.smaller @@ -185,11 +181,7 @@ Item { type: IconButton.Text label.animate: true onClicked: { - const shouldExpand = !sidebarLayout.allSectionsExpanded; - clockSection.expanded = shouldExpand; - barBehaviorSection.expanded = shouldExpand; - traySettingsSection.expanded = shouldExpand; - workspacesSection.expanded = shouldExpand; + // No collapsible sections remaining } } } @@ -258,345 +250,389 @@ Item { ] } - CollapsibleSection { - id: clockSection - title: qsTr("Clock") - - SwitchRow { - label: qsTr("Show clock icon") - checked: root.clockShowIcon - onToggled: checked => { - root.clockShowIcon = checked; - root.saveConfig(); - } - } - } - - CollapsibleSection { - id: barBehaviorSection - title: qsTr("Bar Behavior") + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - SwitchRow { - label: qsTr("Persistent") - checked: root.persistent - onToggled: checked => { - root.persistent = checked; - root.saveConfig(); - } - } + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: 0 - SwitchRow { - label: qsTr("Show on hover") - checked: root.showOnHover - onToggled: checked => { - root.showOnHover = checked; - root.saveConfig(); - } - } + SectionContainer { + Layout.fillWidth: true + alignTop: true - SectionContainer { - contentSpacing: Appearance.spacing.normal + StyledText { + text: qsTr("Workspaces") + font.pointSize: Appearance.font.size.normal + } - ColumnLayout { + StyledRect { Layout.fillWidth: true - spacing: Appearance.spacing.small + implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } RowLayout { - Layout.fillWidth: true + id: workspacesShownRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { - text: qsTr("Drag threshold") - font.pointSize: Appearance.font.size.normal - } - - Item { Layout.fillWidth: true + text: qsTr("Shown") } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: dragThresholdInput.implicitHeight + Appearance.padding.small * 2 - color: dragThresholdInputHover.containsMouse || dragThresholdInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: dragThresholdInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: dragThresholdInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: dragThresholdInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } - - Component.onCompleted: { - text = root.dragThreshold.toString(); - } - - onTextChanged: { - if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - root.dragThreshold = val; - root.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = root.dragThreshold.toString(); - } - } + CustomSpinBox { + min: 1 + max: 20 + value: root.workspacesShown + onValueModified: value => { + root.workspacesShown = value; + root.saveConfig(); } } + } + } - StyledText { - text: "px" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} } - StyledSlider { - id: dragThresholdSlider + RowLayout { + id: workspacesActiveIndicatorRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + StyledText { + Layout.fillWidth: true + text: qsTr("Active indicator") + } - from: 0 - to: 100 - value: root.dragThreshold - onMoved: { - root.dragThreshold = Math.round(dragThresholdSlider.value); - if (!dragThresholdInput.activeFocus) { - dragThresholdInput.text = Math.round(dragThresholdSlider.value).toString(); + StyledSwitch { + checked: root.workspacesActiveIndicator + onToggled: { + root.workspacesActiveIndicator = checked; + root.saveConfig(); } - root.saveConfig(); } } } - } - } - CollapsibleSection { - id: traySettingsSection - title: qsTr("Tray Settings") - - SwitchRow { - label: qsTr("Background") - checked: root.trayBackground - onToggled: checked => { - root.trayBackground = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Compact") - checked: root.trayCompact - onToggled: checked => { - root.trayCompact = checked; - root.saveConfig(); - } - } + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - SwitchRow { - label: qsTr("Recolour") - checked: root.trayRecolour - onToggled: checked => { - root.trayRecolour = checked; - root.saveConfig(); - } - } - } + Behavior on implicitHeight { + Anim {} + } - CollapsibleSection { - id: workspacesSection - title: qsTr("Workspaces") + RowLayout { + id: workspacesOccupiedBgRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + StyledText { + Layout.fillWidth: true + text: qsTr("Occupied background") + } - Behavior on implicitHeight { - Anim {} + StyledSwitch { + checked: root.workspacesOccupiedBg + onToggled: { + root.workspacesOccupiedBg = checked; + root.saveConfig(); + } + } + } } - RowLayout { - id: workspacesShownRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - StyledText { - Layout.fillWidth: true - text: qsTr("Shown") + Behavior on implicitHeight { + Anim {} } - CustomSpinBox { - min: 1 - max: 20 - value: root.workspacesShown - onValueModified: value => { - root.workspacesShown = value; - root.saveConfig(); + RowLayout { + id: workspacesShowWindowsRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Show windows") + } + + StyledSwitch { + checked: root.workspacesShowWindows + onToggled: { + root.workspacesShowWindows = checked; + root.saveConfig(); + } } } } - } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - Behavior on implicitHeight { - Anim {} - } + Behavior on implicitHeight { + Anim {} + } - RowLayout { - id: workspacesActiveIndicatorRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + RowLayout { + id: workspacesPerMonitorRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal - StyledText { - Layout.fillWidth: true - text: qsTr("Active indicator") - } + StyledText { + Layout.fillWidth: true + text: qsTr("Per monitor workspaces") + } - StyledSwitch { - checked: root.workspacesActiveIndicator - onToggled: { - root.workspacesActiveIndicator = checked; - root.saveConfig(); + StyledSwitch { + checked: root.workspacesPerMonitor + onToggled: { + root.workspacesPerMonitor = checked; + root.saveConfig(); + } } } } + } } - StyledRect { + ColumnLayout { Layout.fillWidth: true - implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.small - RowLayout { - id: workspacesOccupiedBgRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + SectionContainer { + Layout.fillWidth: true + alignTop: true StyledText { - Layout.fillWidth: true - text: qsTr("Occupied background") + text: qsTr("Clock") + font.pointSize: Appearance.font.size.normal } - StyledSwitch { - checked: root.workspacesOccupiedBg - onToggled: { - root.workspacesOccupiedBg = checked; + SwitchRow { + label: qsTr("Show clock icon") + checked: root.clockShowIcon + onToggled: checked => { + root.clockShowIcon = checked; root.saveConfig(); } } } + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + ConnectedButtonGroup { + rootItem: root + title: qsTr("Tray Settings") + + options: [ + { + label: qsTr("Background"), + propertyName: "trayBackground", + onToggled: function(checked) { + root.trayBackground = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Compact"), + propertyName: "trayCompact", + onToggled: function(checked) { + root.trayCompact = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Recolour"), + propertyName: "trayRecolour", + onToggled: function(checked) { + root.trayRecolour = checked; + root.saveConfig(); + } + } + ] + } + } } - StyledRect { + ColumnLayout { Layout.fillWidth: true - implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - Behavior on implicitHeight { - Anim {} - } + Layout.alignment: Qt.AlignTop + spacing: 0 - RowLayout { - id: workspacesShowWindowsRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + SectionContainer { + Layout.fillWidth: true + alignTop: true StyledText { - Layout.fillWidth: true - text: qsTr("Show windows") + text: qsTr("Bar Behavior") + font.pointSize: Appearance.font.size.normal } - StyledSwitch { - checked: root.workspacesShowWindows - onToggled: { - root.workspacesShowWindows = checked; + SwitchRow { + label: qsTr("Persistent") + checked: root.persistent + onToggled: checked => { + root.persistent = checked; root.saveConfig(); } } - } - } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + SwitchRow { + label: qsTr("Show on hover") + checked: root.showOnHover + onToggled: checked => { + root.showOnHover = checked; + root.saveConfig(); + } + } - Behavior on implicitHeight { - Anim {} - } + SectionContainer { + contentSpacing: Appearance.spacing.normal - RowLayout { - id: workspacesPerMonitorRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - StyledText { - Layout.fillWidth: true - text: qsTr("Per monitor workspaces") - } + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - StyledSwitch { - checked: root.workspacesPerMonitor - onToggled: { - root.workspacesPerMonitor = checked; - root.saveConfig(); + StyledText { + text: qsTr("Drag threshold") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: dragThresholdInput.implicitHeight + Appearance.padding.small * 2 + color: dragThresholdInputHover.containsMouse || dragThresholdInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: dragThresholdInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: dragThresholdInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: dragThresholdInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 100 } + + Component.onCompleted: { + text = root.dragThreshold.toString(); + } + + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + root.dragThreshold = val; + root.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = root.dragThreshold.toString(); + } + } + } + } + + StyledText { + text: "px" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: dragThresholdSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0 + to: 100 + value: root.dragThreshold + onMoved: { + root.dragThreshold = Math.round(dragThresholdSlider.value); + if (!dragThresholdInput.activeFocus) { + dragThresholdInput.text = Math.round(dragThresholdSlider.value).toString(); + } + root.saveConfig(); + } + } } } } } } + } } } -- cgit v1.2.3-freya From 7a0a9d66812416cc29501b6adea5b3c53d534d17 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 13:16:38 -0500 Subject: controlcenter: correcting padding/margins on containers --- modules/controlcenter/taskbar/TaskbarPane.qml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 3f14dd6..5112b4a 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -122,6 +122,8 @@ Item { id: taskbarClippingRect anchors.fill: parent anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 radius: taskbarBorder.innerRadius color: "transparent" @@ -130,7 +132,9 @@ Item { id: taskbarLoader anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 + 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: taskbarContentComponent -- cgit v1.2.3-freya From b1cc6418499c6a1af9906043c0b60610bb7b2174 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 13:34:29 -0500 Subject: controlcenter: correcting padding/margins on containers --- modules/controlcenter/appearance/AppearancePane.qml | 1 + modules/controlcenter/launcher/LauncherPane.qml | 2 +- modules/controlcenter/taskbar/TaskbarPane.qml | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index b4e93ae..71b4061 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -80,6 +80,7 @@ RowLayout { } Item { + id: leftAppearanceItem Layout.preferredWidth: Math.floor(parent.width * 0.4) Layout.minimumWidth: 420 Layout.fillHeight: true diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 71d1d6f..bf4e85f 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -169,7 +169,6 @@ RowLayout { anchors.margins: Appearance.padding.large + Appearance.padding.normal anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 - anchors.bottomMargin: 0 asynchronous: true sourceComponent: leftContentComponent @@ -186,6 +185,7 @@ RowLayout { id: leftContentComponent ColumnLayout { + id: leftLauncherLayout anchors.fill: parent spacing: Appearance.spacing.small diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 5112b4a..c944154 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -143,6 +143,8 @@ Item { InnerBorder { id: taskbarBorder + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 } Component { -- cgit v1.2.3-freya From 80b3255e2e22374e91f63a3bce0b38bb4ec80159 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 13:37:47 -0500 Subject: controlcenter: corrected tray settings heading --- modules/controlcenter/taskbar/TaskbarPane.qml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index c944154..627caeb 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -471,9 +471,13 @@ Item { Layout.fillWidth: true alignTop: true + StyledText { + text: qsTr("Tray Settings") + font.pointSize: Appearance.font.size.normal + } + ConnectedButtonGroup { rootItem: root - title: qsTr("Tray Settings") options: [ { -- cgit v1.2.3-freya From e3f01909497bd3f056cd1f1a870e00a4e6a6898f Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 13:40:27 -0500 Subject: controlcenter: removed expand/collapse all on taskbar panel --- modules/controlcenter/taskbar/TaskbarPane.qml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 627caeb..b9749ff 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -167,8 +167,6 @@ Item { spacing: Appearance.spacing.small - readonly property bool allSectionsExpanded: true - RowLayout { spacing: Appearance.spacing.smaller @@ -177,19 +175,6 @@ Item { font.pointSize: Appearance.font.size.large font.weight: 500 } - - Item { - Layout.fillWidth: true - } - - IconButton { - icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more" - type: IconButton.Text - label.animate: true - onClicked: { - // No collapsible sections remaining - } - } } ConnectedButtonGroup { -- cgit v1.2.3-freya From 70c1e5f257b9a3d78173bb4ae89ce49e75fad2ab Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 13:46:12 -0500 Subject: controlcenter: updated lock badge colors on networking --- modules/controlcenter/network/NetworkingPane.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index dc568e2..a632c2b 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -380,7 +380,7 @@ Item { implicitWidth: lockIconSize + Appearance.padding.smaller implicitHeight: lockIconSize + Appearance.padding.smaller radius: Appearance.rounding.full - color: (modelData && modelData.active) ? Colours.palette.m3secondaryContainer : Colours.palette.m3surfaceContainerHighest + color: Colours.palette.m3secondaryContainer readonly property real lockIconSize: lockIcon.implicitWidth @@ -391,7 +391,7 @@ Item { text: "lock" font.pointSize: Appearance.font.size.small fill: 1 - color: (modelData && modelData.active) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + color: Colours.palette.m3onSurface } } } -- cgit v1.2.3-freya From 8262d34bc0715d3fabe1690058ab68569a3c03bb Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 13:48:43 -0500 Subject: controlcenter: disable resize on floating window --- modules/controlcenter/WindowFactory.qml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml index c5b7535..abcf5df 100644 --- a/modules/controlcenter/WindowFactory.qml +++ b/modules/controlcenter/WindowFactory.qml @@ -32,12 +32,14 @@ Singleton { destroy(); } - minimumSize.width: 1000 - minimumSize.height: 600 - implicitWidth: cc.implicitWidth implicitHeight: cc.implicitHeight + minimumSize.width: implicitWidth + minimumSize.height: implicitHeight + maximumSize.width: implicitWidth + maximumSize.height: implicitHeight + title: qsTr("Caelestia Settings - %1").arg(cc.active.slice(0, 1).toUpperCase() + cc.active.slice(1)) ControlCenter { -- cgit v1.2.3-freya From ba094963bc62a50e41b444217535218b320b83d0 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 13:55:52 -0500 Subject: controlcenter: added slight background to expanded collapsiblesections on appearance --- components/controls/CollapsibleSection.qml | 16 ++++++++++++++++ modules/controlcenter/appearance/AppearancePane.qml | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 35acdec..a1f038b 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -12,6 +12,7 @@ ColumnLayout { required property string title property string description: "" property bool expanded: false + property bool showBackground: false signal toggleRequested @@ -82,6 +83,21 @@ ColumnLayout { } } + StyledRect { + id: backgroundRect + anchors.fill: parent + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + opacity: root.showBackground && root.expanded ? 1.0 : 0.0 + visible: root.showBackground + + Behavior on opacity { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + ColumnLayout { id: contentColumn anchors.left: parent.left diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 71b4061..56332f1 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -179,6 +179,7 @@ RowLayout { id: themeModeSection title: qsTr("Theme mode") description: qsTr("Light or dark theme") + showBackground: true SwitchRow { label: qsTr("Dark mode") @@ -193,6 +194,7 @@ RowLayout { id: colorVariantSection title: qsTr("Color variant") description: qsTr("Material theme variant") + showBackground: true ColumnLayout { Layout.fillWidth: true @@ -274,6 +276,7 @@ RowLayout { id: colorSchemeSection title: qsTr("Color scheme") description: qsTr("Available color schemes") + showBackground: true ColumnLayout { Layout.fillWidth: true @@ -409,6 +412,7 @@ RowLayout { CollapsibleSection { id: animationsSection title: qsTr("Animations") + showBackground: true SectionContainer { contentSpacing: Appearance.spacing.normal @@ -513,11 +517,13 @@ RowLayout { CollapsibleSection { id: fontsSection title: qsTr("Fonts") + showBackground: true CollapsibleSection { id: materialFontSection title: qsTr("Material font family") expanded: true + showBackground: true Loader { Layout.fillWidth: true @@ -593,6 +599,7 @@ RowLayout { id: monoFontSection title: qsTr("Monospace font family") expanded: false + showBackground: true Loader { Layout.fillWidth: true @@ -668,6 +675,7 @@ RowLayout { id: sansFontSection title: qsTr("Sans-serif font family") expanded: false + showBackground: true Loader { Layout.fillWidth: true @@ -842,6 +850,7 @@ RowLayout { CollapsibleSection { id: scalesSection title: qsTr("Scales") + showBackground: true SectionContainer { contentSpacing: Appearance.spacing.normal @@ -1144,6 +1153,7 @@ RowLayout { CollapsibleSection { id: transparencySection title: qsTr("Transparency") + showBackground: true SwitchRow { label: qsTr("Transparency enabled") @@ -1356,6 +1366,7 @@ RowLayout { CollapsibleSection { id: borderSection title: qsTr("Border") + showBackground: true SectionContainer { contentSpacing: Appearance.spacing.normal @@ -1547,6 +1558,7 @@ RowLayout { CollapsibleSection { id: backgroundSection title: qsTr("Background") + showBackground: true SwitchRow { label: qsTr("Desktop clock") -- cgit v1.2.3-freya From 678f7e33437a39b9499033be1cefe5cd62375291 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 14:16:05 -0500 Subject: controlcenter: inner scrollbar for larger sections such as fonts --- components/controls/StyledScrollBar.qml | 96 ++++++++++++++++++++-- .../controlcenter/appearance/AppearancePane.qml | 14 ++++ 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml index fc641b5..de8b679 100644 --- a/components/controls/StyledScrollBar.qml +++ b/components/controls/StyledScrollBar.qml @@ -19,14 +19,51 @@ ScrollBar { shouldBeActive = flickable.moving; } + property bool _updatingFromFlickable: false + property bool _updatingFromUser: false + + // Sync nonAnimPosition with Qt's automatic position binding onPositionChanged: { - if (position === nonAnimPosition) + if (_updatingFromUser) { + _updatingFromUser = false; + return; + } + if (position === nonAnimPosition) { animating = false; - else if (!animating) + return; + } + if (!animating && !_updatingFromFlickable && !fullMouse.pressed) { nonAnimPosition = position; + } } - position: nonAnimPosition + // Sync nonAnimPosition with flickable when not animating + Connections { + target: flickable + function onContentYChanged() { + if (!animating && !fullMouse.pressed) { + _updatingFromFlickable = true; + const contentHeight = flickable.contentHeight; + const height = flickable.height; + if (contentHeight > height) { + nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); + } else { + nonAnimPosition = 0; + } + _updatingFromFlickable = false; + } + } + } + + Component.onCompleted: { + if (flickable) { + const contentHeight = flickable.contentHeight; + const height = flickable.height; + if (contentHeight > height) { + nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); + } + } + } implicitWidth: Appearance.padding.small contentItem: StyledRect { @@ -86,17 +123,62 @@ ScrollBar { onPressed: event => { root.animating = true; - root.nonAnimPosition = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + root._updatingFromUser = true; + const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] + if (root.flickable) { + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + const maxContentY = contentHeight - height; + const maxPos = 1 - root.size; + const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; + root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); + } + } } - onPositionChanged: event => root.nonAnimPosition = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)) + onPositionChanged: event => { + root._updatingFromUser = true; + const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] + if (root.flickable) { + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + const maxContentY = contentHeight - height; + const maxPos = 1 - root.size; + const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; + root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); + } + } + } function onWheel(event: WheelEvent): void { root.animating = true; + root._updatingFromUser = true; + let newPos = root.nonAnimPosition; if (event.angleDelta.y > 0) - root.nonAnimPosition = Math.max(0, root.nonAnimPosition - 0.1); + newPos = Math.max(0, root.nonAnimPosition - 0.1); else if (event.angleDelta.y < 0) - root.nonAnimPosition = Math.min(1 - root.size, root.nonAnimPosition + 0.1); + newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] + if (root.flickable) { + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + const maxContentY = contentHeight - height; + const maxPos = 1 - root.size; + const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; + root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); + } + } } } diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 56332f1..177e7b9 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -526,6 +526,7 @@ RowLayout { showBackground: true Loader { + id: materialFontLoader Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 asynchronous: true @@ -539,6 +540,10 @@ RowLayout { spacing: Appearance.spacing.small / 2 model: Qt.fontFamilies() + StyledScrollBar.vertical: StyledScrollBar { + flickable: materialFontList + } + delegate: StyledRect { required property string modelData required property int index @@ -592,6 +597,7 @@ RowLayout { implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 } } + } } @@ -615,6 +621,10 @@ RowLayout { spacing: Appearance.spacing.small / 2 model: Qt.fontFamilies() + StyledScrollBar.vertical: StyledScrollBar { + flickable: monoFontList + } + delegate: StyledRect { required property string modelData required property int index @@ -691,6 +701,10 @@ RowLayout { spacing: Appearance.spacing.small / 2 model: Qt.fontFamilies() + StyledScrollBar.vertical: StyledScrollBar { + flickable: sansFontList + } + delegate: StyledRect { required property string modelData required property int index -- cgit v1.2.3-freya From d92911b7e4715adba518cf560f50c81ee520fcca Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 14:24:32 -0500 Subject: controlcenter: changed button groups to match other elements --- modules/controlcenter/taskbar/TaskbarPane.qml | 131 ++++++++++++++------------ 1 file changed, 70 insertions(+), 61 deletions(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index b9749ff..3798adb 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -177,68 +177,77 @@ Item { } } - ConnectedButtonGroup { - rootItem: root - title: qsTr("Status Icons") - - options: [ - { - label: qsTr("Audio"), - propertyName: "showAudio", - onToggled: function(checked) { - root.showAudio = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Microphone"), - propertyName: "showMicrophone", - onToggled: function(checked) { - root.showMicrophone = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Keyboard"), - propertyName: "showKbLayout", - onToggled: function(checked) { - root.showKbLayout = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Network"), - propertyName: "showNetwork", - onToggled: function(checked) { - root.showNetwork = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Bluetooth"), - propertyName: "showBluetooth", - onToggled: function(checked) { - root.showBluetooth = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Battery"), - propertyName: "showBattery", - onToggled: function(checked) { - root.showBattery = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Capslock"), - propertyName: "showLockStatus", - onToggled: function(checked) { - root.showLockStatus = checked; - root.saveConfig(); + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Status Icons") + font.pointSize: Appearance.font.size.normal + } + + ConnectedButtonGroup { + rootItem: root + + options: [ + { + label: qsTr("Audio"), + propertyName: "showAudio", + onToggled: function(checked) { + root.showAudio = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Microphone"), + propertyName: "showMicrophone", + onToggled: function(checked) { + root.showMicrophone = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Keyboard"), + propertyName: "showKbLayout", + onToggled: function(checked) { + root.showKbLayout = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Network"), + propertyName: "showNetwork", + onToggled: function(checked) { + root.showNetwork = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Bluetooth"), + propertyName: "showBluetooth", + onToggled: function(checked) { + root.showBluetooth = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Battery"), + propertyName: "showBattery", + onToggled: function(checked) { + root.showBattery = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Capslock"), + propertyName: "showLockStatus", + onToggled: function(checked) { + root.showLockStatus = checked; + root.saveConfig(); + } } - } - ] + ] + } } RowLayout { -- cgit v1.2.3-freya From 58e657d891986598d1d4d5ea064e4130532c98ac Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 14:27:16 -0500 Subject: controlcenter: changed button label for audio to speakers --- modules/controlcenter/taskbar/TaskbarPane.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 3798adb..b798da8 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -191,7 +191,7 @@ Item { options: [ { - label: qsTr("Audio"), + label: qsTr("Speakers"), propertyName: "showAudio", onToggled: function(checked) { root.showAudio = checked; -- cgit v1.2.3-freya From 23e5d7d13ce0432e17a7e2077b12b44278f919b6 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 15:46:40 -0500 Subject: controlcenter: renamed panel titles from settings to panel name --- modules/controlcenter/appearance/AppearancePane.qml | 2 +- modules/controlcenter/audio/AudioPane.qml | 4 ++-- modules/controlcenter/bluetooth/DeviceList.qml | 2 +- modules/controlcenter/launcher/LauncherPane.qml | 2 +- modules/controlcenter/network/NetworkingPane.qml | 4 ++-- modules/controlcenter/taskbar/TaskbarPane.qml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 177e7b9..891f64b 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -147,7 +147,7 @@ RowLayout { spacing: Appearance.spacing.smaller StyledText { - text: qsTr("Settings") + text: qsTr("Appearance") font.pointSize: Appearance.font.size.large font.weight: 500 } diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 3440a2f..c2d60d8 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -74,13 +74,13 @@ RowLayout { anchors.right: parent.right spacing: Appearance.spacing.normal - // Settings header above the collapsible sections + // Audio header above the collapsible sections RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller StyledText { - text: qsTr("Settings") + text: qsTr("Audio") font.pointSize: Appearance.font.size.large font.weight: 500 } diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index 8e79e72..06700e8 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -25,7 +25,7 @@ ColumnLayout { spacing: Appearance.spacing.smaller StyledText { - text: qsTr("Settings") + text: qsTr("Bluetooth") font.pointSize: Appearance.font.size.large font.weight: 500 } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index bf4e85f..300117a 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -194,7 +194,7 @@ RowLayout { spacing: Appearance.spacing.smaller StyledText { - text: qsTr("Settings") + text: qsTr("Launcher") font.pointSize: Appearance.font.size.large font.weight: 500 } diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index a632c2b..52499d8 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -82,13 +82,13 @@ Item { anchors.right: parent.right spacing: Appearance.spacing.normal - // Settings header above the collapsible sections + // Network header above the collapsible sections RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller StyledText { - text: qsTr("Settings") + text: qsTr("Network") font.pointSize: Appearance.font.size.large font.weight: 500 } diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index b798da8..507a239 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -171,7 +171,7 @@ Item { spacing: Appearance.spacing.smaller StyledText { - text: qsTr("Settings") + text: qsTr("Taskbar") font.pointSize: Appearance.font.size.large font.weight: 500 } -- cgit v1.2.3-freya From 34abc91523de0c2632f725e2b2ffb39e8e0ef18d Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 19:27:45 -0500 Subject: controlcenter: prevented changing panes before opening animation completes --- modules/controlcenter/ControlCenter.qml | 11 +++++ modules/controlcenter/NavRail.qml | 5 ++ modules/controlcenter/Panes.qml | 82 ++++++++++++++++++++++----------- 3 files changed, 71 insertions(+), 27 deletions(-) diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index 8cdf01f..3642a33 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -64,6 +64,11 @@ Item { anchors.fill: parent function onWheel(event: WheelEvent): void { + // Prevent tab switching during initial opening animation to avoid blank pages + if (!panes.initialOpeningComplete) { + return; + } + if (event.angleDelta.y < 0) root.session.activeIndex = Math.min(root.session.activeIndex + 1, root.session.panes.length - 1); else if (event.angleDelta.y > 0) @@ -76,10 +81,13 @@ Item { screen: root.screen session: root.session + initialOpeningComplete: root.initialOpeningComplete } } Panes { + id: panes + Layout.fillWidth: true Layout.fillHeight: true @@ -88,4 +96,7 @@ Item { session: root.session } } + + // Expose initialOpeningComplete for NavRail to prevent tab switching during opening animation + readonly property bool initialOpeningComplete: panes.initialOpeningComplete } diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index d1d432d..1de1a9e 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -13,6 +13,7 @@ Item { required property ShellScreen screen required property Session session + required property bool initialOpeningComplete implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4 implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 @@ -197,6 +198,10 @@ Item { color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface function onClicked(): void { + // Prevent tab switching during initial opening animation to avoid blank pages + if (!root.initialOpeningComplete) { + return; + } root.session.active = item.label; } } diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index e97a3fe..756d73a 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -18,6 +18,9 @@ ClippingRectangle { required property Session session + // Expose initialOpeningComplete so parent can check if opening animation is done + readonly property bool initialOpeningComplete: layout.initialOpeningComplete + color: "transparent" clip: true focus: false @@ -140,6 +143,32 @@ ClippingRectangle { // Track if this pane has ever been loaded to enable caching property bool hasBeenLoaded: false + + // Function to compute if this pane should be active + function updateActive(): void { + const diff = Math.abs(root.session.activeIndex - pane.index); + const isActivePane = diff === 0; + let shouldBeActive = false; + + // During initial opening animation, only load the active pane + // This prevents hiccups from multiple panes loading simultaneously + if (!layout.initialOpeningComplete) { + shouldBeActive = isActivePane; + } else { + // After initial opening, allow current and adjacent panes for smooth transitions + if (diff <= 1) { + shouldBeActive = true; + } else if (pane.hasBeenLoaded) { + // For distant panes that have been loaded before, keep them active to preserve cached data + shouldBeActive = true; + } else { + // For new distant panes, wait until animation completes to avoid heavy loading during transition + shouldBeActive = layout.animationComplete; + } + } + + loader.active = shouldBeActive; + } Loader { id: loader @@ -147,35 +176,17 @@ ClippingRectangle { anchors.fill: parent clip: false asynchronous: true - active: { - const diff = Math.abs(root.session.activeIndex - pane.index); - const isActivePane = diff === 0; - - // During initial opening animation, only load the active pane - // This prevents hiccups from multiple panes loading simultaneously - if (!layout.initialOpeningComplete) { - if (isActivePane) { - pane.hasBeenLoaded = true; - return true; - } - // Defer all other panes until initial opening completes - return false; - } - - // After initial opening, allow current and adjacent panes for smooth transitions - if (diff <= 1) { + active: false + + Component.onCompleted: { + pane.updateActive(); + } + + onActiveChanged: { + // Mark pane as loaded when it becomes active + if (active && !pane.hasBeenLoaded) { pane.hasBeenLoaded = true; - return true; - } - - // For distant panes that have been loaded before, keep them active to preserve cached data - // Only wait for animation if pane hasn't been loaded yet - if (pane.hasBeenLoaded) { - return true; } - - // For new distant panes, wait until animation completes to avoid heavy loading during transition - return layout.animationComplete; } onItemChanged: { @@ -185,5 +196,22 @@ ClippingRectangle { } } } + + Connections { + target: root.session + function onActiveIndexChanged(): void { + pane.updateActive(); + } + } + + Connections { + target: layout + function onInitialOpeningCompleteChanged(): void { + pane.updateActive(); + } + function onAnimationCompleteChanged(): void { + pane.updateActive(); + } + } } } -- cgit v1.2.3-freya From b54ce46b986b39450786f3aec69e2b9aca6cdc5d Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 19:31:31 -0500 Subject: controlcenter: corrected spacing between collapsiblesection headers and content --- components/controls/CollapsibleSection.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index a1f038b..078145b 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -16,7 +16,7 @@ ColumnLayout { signal toggleRequested - spacing: 0 + spacing: Appearance.spacing.small Layout.fillWidth: true Item { -- cgit v1.2.3-freya From ee33f8f07bade777e84b3727647a7d5eb89a29e5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 19:44:31 -0500 Subject: controlcenter: corrected timing of network panel animations --- modules/controlcenter/network/EthernetPane.qml | 16 ++++++++++++++-- modules/controlcenter/network/NetworkingPane.qml | 20 ++++++++++++++++++-- modules/controlcenter/network/WirelessPane.qml | 16 ++++++++++++++-- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml index 05d0b1b..6a50cde 100644 --- a/modules/controlcenter/network/EthernetPane.qml +++ b/modules/controlcenter/network/EthernetPane.qml @@ -57,6 +57,8 @@ RowLayout { property var pane: root.session.ethernet.active property string paneId: pane ? (pane.interface || "") : "" + property Component targetComponent: settings + property Component nextComponent: settings anchors.fill: parent anchors.margins: Appearance.padding.large * 2 @@ -67,7 +69,12 @@ RowLayout { clip: true asynchronous: true - sourceComponent: pane ? details : settings + sourceComponent: loader.targetComponent + + Component.onCompleted: { + targetComponent = pane ? details : settings; + nextComponent = targetComponent; + } Behavior on paneId { SequentialAnimation { @@ -85,7 +92,11 @@ RowLayout { easing.bezierCurve: Appearance.anim.curves.standardAccel } } - PropertyAction {} + PropertyAction { + target: loader + property: "targetComponent" + value: loader.nextComponent + } ParallelAnimation { Anim { target: loader @@ -104,6 +115,7 @@ RowLayout { } onPaneChanged: { + nextComponent = pane ? details : settings; paneId = pane ? (pane.interface || "") : ""; } } diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 52499d8..47bb7fb 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -492,6 +492,12 @@ Item { 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; + } anchors.fill: parent anchors.margins: Appearance.padding.large * 2 @@ -502,7 +508,12 @@ Item { clip: false asynchronous: true - sourceComponent: pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings + sourceComponent: loader.targetComponent + + Component.onCompleted: { + targetComponent = getComponentForPane(); + nextComponent = targetComponent; + } Behavior on paneId { SequentialAnimation { @@ -520,7 +531,11 @@ Item { easing.bezierCurve: Appearance.anim.curves.standardAccel } } - PropertyAction {} + PropertyAction { + target: loader + property: "targetComponent" + value: loader.nextComponent + } ParallelAnimation { Anim { target: loader @@ -539,6 +554,7 @@ Item { } onPaneChanged: { + nextComponent = getComponentForPane(); paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); } } diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index 67a00f7..22364a1 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -56,6 +56,8 @@ RowLayout { property var pane: root.session.network.active property string paneId: pane ? (pane.ssid || pane.bssid || "") : "" + property Component targetComponent: settings + property Component nextComponent: settings anchors.fill: parent anchors.margins: Appearance.padding.large * 2 @@ -66,7 +68,12 @@ RowLayout { clip: false asynchronous: true - sourceComponent: pane ? details : settings + sourceComponent: loader.targetComponent + + Component.onCompleted: { + targetComponent = pane ? details : settings; + nextComponent = targetComponent; + } Behavior on paneId { SequentialAnimation { @@ -84,7 +91,11 @@ RowLayout { easing.bezierCurve: Appearance.anim.curves.standardAccel } } - PropertyAction {} + PropertyAction { + target: loader + property: "targetComponent" + value: loader.nextComponent + } ParallelAnimation { Anim { target: loader @@ -103,6 +114,7 @@ RowLayout { } onPaneChanged: { + nextComponent = pane ? details : settings; paneId = pane ? (pane.ssid || pane.bssid || "") : ""; } } -- cgit v1.2.3-freya From 52b1b79abda0c089e782e06e6e4ce3b50a7f73b4 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 20:03:31 -0500 Subject: controlcenter: added elevation effect to networking pane for improved visual hierarchy --- modules/controlcenter/network/NetworkingPane.qml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 47bb7fb..d0ea852 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -384,6 +384,13 @@ Item { readonly property real lockIconSize: lockIcon.implicitWidth + Elevation { + anchors.fill: parent + radius: parent.radius + z: -1 + level: 2 + } + MaterialIcon { id: lockIcon -- cgit v1.2.3-freya From 3f4d07bfc7d942e82b5fe7b0650c2e5a269554f0 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 20:10:02 -0500 Subject: controlcenter: padding/margins on taskbar panel --- modules/controlcenter/taskbar/TaskbarPane.qml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 507a239..18d5304 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -165,7 +165,7 @@ Item { anchors.right: parent.right anchors.top: parent.top - spacing: Appearance.spacing.small + spacing: Appearance.spacing.normal RowLayout { spacing: Appearance.spacing.smaller @@ -252,12 +252,12 @@ Item { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Appearance.spacing.normal ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: 0 + spacing: Appearance.spacing.small SectionContainer { Layout.fillWidth: true @@ -440,7 +440,7 @@ Item { ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: Appearance.spacing.small + spacing: Appearance.spacing.normal SectionContainer { Layout.fillWidth: true @@ -506,7 +506,7 @@ Item { ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: 0 + spacing: Appearance.spacing.small SectionContainer { Layout.fillWidth: true -- cgit v1.2.3-freya From 4de4a787def43587a230ae3de10febc37b34d0ac Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 22:01:40 -0500 Subject: controlcenter: launcher pane refreshment --- modules/controlcenter/Session.qml | 5 + modules/controlcenter/launcher/LauncherPane.qml | 133 +++++++++++++- modules/controlcenter/launcher/Settings.qml | 226 ++++++++++++++++++++++++ 3 files changed, 357 insertions(+), 7 deletions(-) create mode 100644 modules/controlcenter/launcher/Settings.qml diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index efd360d..6f560f5 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -13,6 +13,7 @@ QtObject { readonly property Bt bt: Bt {} readonly property Network network: Network {} readonly property Ethernet ethernet: Ethernet {} + readonly property Launcher launcher: Launcher {} onActiveChanged: activeIndex = panes.indexOf(active) onActiveIndexChanged: active = panes[activeIndex] @@ -34,4 +35,8 @@ QtObject { component Ethernet: QtObject { property var active } + + component Launcher: QtObject { + property var active + } } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 300117a..85db6df 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -21,13 +21,26 @@ RowLayout { required property Session session - property var selectedApp: null + property var selectedApp: root.session.launcher.active property bool hideFromLauncherChecked: false anchors.fill: parent spacing: 0 + onSelectedAppChanged: { + root.session.launcher.active = root.selectedApp; + updateToggleState(); + } + + Connections { + target: root.session.launcher + function onActiveChanged() { + root.selectedApp = root.session.launcher.active; + updateToggleState(); + } + } + function updateToggleState() { if (!root.selectedApp) { root.hideFromLauncherChecked = false; @@ -73,9 +86,6 @@ RowLayout { Config.save(); } - onSelectedAppChanged: { - updateToggleState(); - } AppDb { id: allAppsDb @@ -202,6 +212,23 @@ RowLayout { Item { Layout.fillWidth: true } + + ToggleButton { + toggled: !root.session.launcher.active + icon: "settings" + accent: "Primary" + + onClicked: { + if (root.session.launcher.active) { + root.session.launcher.active = null; + } else { + // Toggle to show settings - if there are apps, select the first one, otherwise show settings + if (root.filteredApps.length > 0) { + root.session.launcher.active = root.filteredApps[0]; + } + } + } + } } StyledText { @@ -351,7 +378,7 @@ RowLayout { StateLayer { function onClicked(): void { - root.selectedApp = modelData; + root.session.launcher.active = modelData; } } @@ -405,11 +432,74 @@ RowLayout { Loader { id: rightLauncherLoader + property var pane: root.session.launcher.active + property string paneId: pane ? (pane.id || pane.entry?.id || "") : "" + anchors.fill: parent anchors.margins: Appearance.padding.large * 2 + opacity: 1 + scale: 1 + transformOrigin: Item.Center + clip: false + asynchronous: true - sourceComponent: rightContentComponent + sourceComponent: pane ? appDetails : settings + + Component.onCompleted: { + targetComponent = pane ? appDetails : settings; + nextComponent = targetComponent; + } + + property Component targetComponent: settings + property Component nextComponent: settings + + function getComponentForPane() { + return pane ? appDetails : settings; + } + + Behavior on paneId { + SequentialAnimation { + ParallelAnimation { + Anim { + target: rightLauncherLoader + property: "opacity" + to: 0 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + Anim { + target: rightLauncherLoader + property: "scale" + to: 0.8 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + } + PropertyAction { + target: rightLauncherLoader + property: "targetComponent" + value: rightLauncherLoader.nextComponent + } + ParallelAnimation { + Anim { + target: rightLauncherLoader + property: "opacity" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + target: rightLauncherLoader + property: "scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + } + } + + onPaneChanged: { + nextComponent = getComponentForPane(); + paneId = pane ? (pane.id || pane.entry?.id || "") : ""; + } } } @@ -420,7 +510,30 @@ RowLayout { } Component { - id: rightContentComponent + 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 { + id: appDetails ColumnLayout { anchors.fill: parent @@ -516,4 +629,10 @@ RowLayout { } } } + + component Anim: NumberAnimation { + target: rightLauncherLoader + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + } } diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml new file mode 100644 index 0000000..1fef7f5 --- /dev/null +++ b/modules/controlcenter/launcher/Settings.qml @@ -0,0 +1,226 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "apps" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Launcher Settings") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("General") + description: qsTr("General launcher settings") + } + + SectionContainer { + ToggleRow { + label: qsTr("Enabled") + checked: Config.launcher.enabled + toggle.onToggled: { + Config.launcher.enabled = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Show on hover") + checked: Config.launcher.showOnHover + toggle.onToggled: { + Config.launcher.showOnHover = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Vim keybinds") + checked: Config.launcher.vimKeybinds + toggle.onToggled: { + Config.launcher.vimKeybinds = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Enable dangerous actions") + checked: Config.launcher.enableDangerousActions + toggle.onToggled: { + Config.launcher.enableDangerousActions = checked; + Config.save(); + } + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Display") + description: qsTr("Display and appearance settings") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Max shown items") + value: qsTr("%1").arg(Config.launcher.maxShown) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Max wallpapers") + value: qsTr("%1").arg(Config.launcher.maxWallpapers) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Drag threshold") + value: qsTr("%1 px").arg(Config.launcher.dragThreshold) + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Prefixes") + description: qsTr("Command prefix settings") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Special prefix") + value: Config.launcher.specialPrefix || qsTr("None") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Action prefix") + value: Config.launcher.actionPrefix || qsTr("None") + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Fuzzy search") + description: qsTr("Fuzzy search settings") + } + + SectionContainer { + ToggleRow { + label: qsTr("Apps") + checked: Config.launcher.useFuzzy.apps + toggle.onToggled: { + Config.launcher.useFuzzy.apps = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Actions") + checked: Config.launcher.useFuzzy.actions + toggle.onToggled: { + Config.launcher.useFuzzy.actions = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Schemes") + checked: Config.launcher.useFuzzy.schemes + toggle.onToggled: { + Config.launcher.useFuzzy.schemes = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Variants") + checked: Config.launcher.useFuzzy.variants + toggle.onToggled: { + Config.launcher.useFuzzy.variants = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Wallpapers") + checked: Config.launcher.useFuzzy.wallpapers + toggle.onToggled: { + Config.launcher.useFuzzy.wallpapers = checked; + Config.save(); + } + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Sizes") + description: qsTr("Size settings for launcher items") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Item width") + value: qsTr("%1 px").arg(Config.launcher.sizes.itemWidth) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Item height") + value: qsTr("%1 px").arg(Config.launcher.sizes.itemHeight) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Wallpaper width") + value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperWidth) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Wallpaper height") + value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperHeight) + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Hidden apps") + description: qsTr("Applications hidden from launcher") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Total hidden") + value: qsTr("%1").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0) + } + } +} + -- cgit v1.2.3-freya From e72fe3f73b4e9eb9dd3e502458090839e86b947f Mon Sep 17 00:00:00 2001 From: ATMDA Date: Mon, 17 Nov 2025 22:37:33 -0500 Subject: controlcenter: corrected launcher animation on data update --- modules/controlcenter/launcher/LauncherPane.qml | 80 ++++++++++++++++++++----- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 85db6df..f2247a7 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -434,6 +434,13 @@ RowLayout { property var pane: root.session.launcher.active property string paneId: pane ? (pane.id || pane.entry?.id || "") : "" + property Component targetComponent: settings + property Component nextComponent: settings + property var displayedApp: null + + function getComponentForPane() { + return pane ? appDetails : settings; + } anchors.fill: parent anchors.margins: Appearance.padding.large * 2 @@ -444,18 +451,20 @@ RowLayout { clip: false asynchronous: true - sourceComponent: pane ? appDetails : settings + sourceComponent: rightLauncherLoader.targetComponent + active: true Component.onCompleted: { - targetComponent = pane ? appDetails : settings; + displayedApp = pane; + targetComponent = getComponentForPane(); nextComponent = targetComponent; } - property Component targetComponent: settings - property Component nextComponent: settings - - function getComponentForPane() { - return pane ? appDetails : settings; + onItemChanged: { + // Ensure displayedApp is set when item is created (for async loading) + if (item && pane && displayedApp !== pane) { + displayedApp = pane; + } } Behavior on paneId { @@ -474,11 +483,26 @@ RowLayout { easing.bezierCurve: Appearance.anim.curves.standardAccel } } + PropertyAction { + target: rightLauncherLoader + property: "displayedApp" + value: rightLauncherLoader.pane + } + PropertyAction { + target: rightLauncherLoader + property: "active" + value: false + } PropertyAction { target: rightLauncherLoader property: "targetComponent" value: rightLauncherLoader.nextComponent } + PropertyAction { + target: rightLauncherLoader + property: "active" + value: true + } ParallelAnimation { Anim { target: rightLauncherLoader @@ -500,6 +524,19 @@ RowLayout { nextComponent = getComponentForPane(); paneId = pane ? (pane.id || pane.entry?.id || "") : ""; } + + onDisplayedAppChanged: { + if (displayedApp) { + const appId = displayedApp.id || displayedApp.entry?.id; + if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { + root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); + } else { + root.hideFromLauncherChecked = false; + } + } else { + root.hideFromLauncherChecked = false; + } + } } } @@ -550,7 +587,7 @@ RowLayout { Loader { id: iconLoader - sourceComponent: root.selectedApp ? appIconComponent : defaultIconComponent + sourceComponent: rightLauncherLoader.displayedApp ? appIconComponent : defaultIconComponent } Component { @@ -558,8 +595,8 @@ RowLayout { IconImage { implicitSize: Appearance.font.size.extraLarge * 3 * 2 source: { - if (!root.selectedApp) return "image-missing"; - const entry = root.selectedApp.entry; + if (!rightLauncherLoader.displayedApp) return "image-missing"; + const entry = rightLauncherLoader.displayedApp.entry; if (entry && entry.icon) { return Quickshell.iconPath(entry.icon, "image-missing"); } @@ -582,7 +619,7 @@ RowLayout { Layout.alignment: Qt.AlignHCenter Layout.leftMargin: Appearance.padding.large * 2 Layout.rightMargin: Appearance.padding.large * 2 - text: root.selectedApp ? (root.selectedApp.name || root.selectedApp.entry?.name || qsTr("Application Details")) : qsTr("Launcher Applications") + 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 } @@ -613,13 +650,28 @@ RowLayout { SwitchRow { Layout.topMargin: Appearance.spacing.normal - visible: root.selectedApp !== null + visible: rightLauncherLoader.displayedApp !== null label: qsTr("Hide from launcher") checked: root.hideFromLauncherChecked - enabled: root.selectedApp !== null + enabled: rightLauncherLoader.displayedApp !== null onToggled: checked => { root.hideFromLauncherChecked = checked; - root.saveHiddenApps(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); + } + } + Config.launcher.hiddenApps = hiddenApps; + Config.save(); + } } } -- cgit v1.2.3-freya From 7ac403d93304f122b051ad3c9ef794941648d1f5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 18 Nov 2025 12:10:41 -0500 Subject: controlcenter: added more missing options to taskbar panel --- .../controlcenter/taskbar/ConnectedButtonGroup.qml | 18 ++++ modules/controlcenter/taskbar/TaskbarPane.qml | 112 +++++++++++++++++++-- 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml b/modules/controlcenter/taskbar/ConnectedButtonGroup.qml index e35ccfc..af386c3 100644 --- a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml +++ b/modules/controlcenter/taskbar/ConnectedButtonGroup.qml @@ -131,6 +131,24 @@ StyledRect { button.isChecked = root.rootItem.trayRecolour; } } + + function onScrollWorkspacesChanged() { + if (modelData.propertyName === "scrollWorkspaces") { + button.isChecked = root.rootItem.scrollWorkspaces; + } + } + + function onScrollVolumeChanged() { + if (modelData.propertyName === "scrollVolume") { + button.isChecked = root.rootItem.scrollVolume; + } + } + + function onScrollBrightnessChanged() { + if (modelData.propertyName === "scrollBrightness") { + button.isChecked = root.rootItem.scrollBrightness; + } + } } // Match utilities Toggles radius styling diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 18d5304..e84e5fe 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -47,6 +47,16 @@ Item { property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true + // Scroll Actions + property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true + property bool scrollVolume: Config.bar.scrollActions.volume ?? true + property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true + + // Popouts + property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true + property bool popoutTray: Config.bar.popouts.tray ?? true + property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true + anchors.fill: parent Component.onCompleted: { @@ -93,6 +103,16 @@ Item { Config.bar.workspaces.showWindows = root.workspacesShowWindows; Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; + // Update scroll actions + Config.bar.scrollActions.workspaces = root.scrollWorkspaces; + Config.bar.scrollActions.volume = root.scrollVolume; + Config.bar.scrollActions.brightness = root.scrollBrightness; + + // Update popouts + Config.bar.popouts.activeWindow = root.popoutActiveWindow; + Config.bar.popouts.tray = root.popoutTray; + Config.bar.popouts.statusIcons = root.popoutStatusIcons; + // Update entries from the model (same approach as clock - use provided value if available) const entries = []; for (let i = 0; i < entriesModel.count; i++) { @@ -257,7 +277,7 @@ Item { ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: Appearance.spacing.small + spacing: Appearance.spacing.normal SectionContainer { Layout.fillWidth: true @@ -435,6 +455,47 @@ Item { } } } + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Scroll Actions") + font.pointSize: Appearance.font.size.normal + } + + ConnectedButtonGroup { + rootItem: root + + options: [ + { + label: qsTr("Workspaces"), + propertyName: "scrollWorkspaces", + onToggled: function(checked) { + root.scrollWorkspaces = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Volume"), + propertyName: "scrollVolume", + onToggled: function(checked) { + root.scrollVolume = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Brightness"), + propertyName: "scrollBrightness", + onToggled: function(checked) { + root.scrollBrightness = checked; + root.saveConfig(); + } + } + ] + } + } } ColumnLayout { @@ -501,12 +562,6 @@ Item { ] } } - } - - ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Appearance.spacing.small SectionContainer { Layout.fillWidth: true @@ -635,6 +690,49 @@ Item { } } } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Popouts") + font.pointSize: Appearance.font.size.normal + } + + SwitchRow { + label: qsTr("Active window") + checked: root.popoutActiveWindow + onToggled: checked => { + root.popoutActiveWindow = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Tray") + checked: root.popoutTray + onToggled: checked => { + root.popoutTray = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Status icons") + checked: root.popoutStatusIcons + onToggled: checked => { + root.popoutStatusIcons = checked; + root.saveConfig(); + } + } + } + } } } -- cgit v1.2.3-freya From dedff0bfe5cc6cc4f4081782daed8eb7963f1657 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Tue, 18 Nov 2025 12:21:31 -0500 Subject: controlcenter: minor moving around elements in taskbar panel --- modules/controlcenter/taskbar/TaskbarPane.qml | 82 +++++++++++++-------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index e84e5fe..1c3adbc 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -522,47 +522,6 @@ Item { } } - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Tray Settings") - font.pointSize: Appearance.font.size.normal - } - - ConnectedButtonGroup { - rootItem: root - - options: [ - { - label: qsTr("Background"), - propertyName: "trayBackground", - onToggled: function(checked) { - root.trayBackground = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Compact"), - propertyName: "trayCompact", - onToggled: function(checked) { - root.trayCompact = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Recolour"), - propertyName: "trayRecolour", - onToggled: function(checked) { - root.trayRecolour = checked; - root.saveConfig(); - } - } - ] - } - } - SectionContainer { Layout.fillWidth: true alignTop: true @@ -732,6 +691,47 @@ Item { } } } + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Tray Settings") + font.pointSize: Appearance.font.size.normal + } + + ConnectedButtonGroup { + rootItem: root + + options: [ + { + label: qsTr("Background"), + propertyName: "trayBackground", + onToggled: function(checked) { + root.trayBackground = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Compact"), + propertyName: "trayCompact", + onToggled: function(checked) { + root.trayCompact = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Recolour"), + propertyName: "trayRecolour", + onToggled: function(checked) { + root.trayRecolour = checked; + root.saveConfig(); + } + } + ] + } + } } } -- cgit v1.2.3-freya From 50e29f8be35cb773b5bf25a494eddfe665073583 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 08:01:48 -0500 Subject: controlcenter: corrected nesting of bg when transparency off --- components/SectionContainer.qml | 4 +++- components/controls/CollapsibleSection.qml | 5 ++++- modules/controlcenter/appearance/AppearancePane.qml | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml index 60e3d59..f133e19 100644 --- a/components/SectionContainer.qml +++ b/components/SectionContainer.qml @@ -16,7 +16,9 @@ StyledRect { implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + color: Colours.transparency.enabled + ? Colours.layer(Colours.palette.m3surfaceContainer, 2) + : Colours.palette.m3surfaceContainerHigh ColumnLayout { id: contentColumn diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 078145b..8940884 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -13,6 +13,7 @@ ColumnLayout { property string description: "" property bool expanded: false property bool showBackground: false + property bool nested: false signal toggleRequested @@ -87,7 +88,9 @@ ColumnLayout { id: backgroundRect anchors.fill: parent radius: Appearance.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + color: Colours.transparency.enabled + ? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2) + : (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer) opacity: root.showBackground && root.expanded ? 1.0 : 0.0 visible: root.showBackground diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 891f64b..61cdcaa 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -524,6 +524,7 @@ RowLayout { title: qsTr("Material font family") expanded: true showBackground: true + nested: true Loader { id: materialFontLoader @@ -606,6 +607,7 @@ RowLayout { title: qsTr("Monospace font family") expanded: false showBackground: true + nested: true Loader { Layout.fillWidth: true @@ -686,6 +688,7 @@ RowLayout { title: qsTr("Sans-serif font family") expanded: false showBackground: true + nested: true Loader { Layout.fillWidth: true -- cgit v1.2.3-freya From ad4213d45ccf227e3528dd2bcb992ec75ab8d0c1 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 12:39:45 -0500 Subject: refactor: SplitPaneLayout now component --- .../controlcenter/appearance/AppearancePane.qml | 3205 ++++++++++---------- modules/controlcenter/audio/AudioPane.qml | 570 ++-- modules/controlcenter/bluetooth/BtPane.qml | 138 +- .../controlcenter/components/SplitPaneLayout.qml | 120 + modules/controlcenter/launcher/LauncherPane.qml | 288 +- modules/controlcenter/network/NetworkingPane.qml | 296 +- 6 files changed, 2249 insertions(+), 2368 deletions(-) create mode 100644 modules/controlcenter/components/SplitPaneLayout.qml diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 61cdcaa..2041bf8 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import "../../launcher/services" import qs.components import qs.components.controls @@ -16,7 +17,7 @@ import Quickshell.Widgets import QtQuick import QtQuick.Layouts -RowLayout { +Item { id: root required property Session session @@ -46,9 +47,6 @@ RowLayout { anchors.fill: parent - spacing: 0 - - function saveConfig() { Config.appearance.anim.durations.scale = root.animDurationsScale; @@ -79,794 +77,920 @@ RowLayout { Config.save(); } - Item { - id: leftAppearanceItem - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - ClippingRectangle { - id: leftAppearanceClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - radius: leftAppearanceBorder.innerRadius - color: "transparent" - - Loader { - id: leftAppearanceLoader - anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 - asynchronous: true - sourceComponent: appearanceLeftContentComponent - property var rootPane: root - } - } + Component { + id: appearanceRightContentComponent - InnerBorder { - id: leftAppearanceBorder - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } + StyledFlickable { + id: rightAppearanceFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height - Component { - id: appearanceLeftContentComponent + StyledScrollBar.vertical: StyledScrollBar { + flickable: rightAppearanceFlickable + } - StyledFlickable { - id: sidebarFlickable - readonly property var rootPane: leftAppearanceLoader.rootPane - flickableDirection: Flickable.VerticalFlick - contentHeight: sidebarLayout.height + ColumnLayout { + id: contentLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal - StyledScrollBar.vertical: StyledScrollBar { - flickable: sidebarFlickable + MaterialIcon { + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.topMargin: 0 + text: "palette" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true } - ColumnLayout { - id: sidebarLayout - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small - - readonly property bool allSectionsExpanded: - themeModeSection.expanded && - colorVariantSection.expanded && - colorSchemeSection.expanded && - animationsSection.expanded && - fontsSection.expanded && - scalesSection.expanded && - transparencySection.expanded && - borderSection.expanded && - backgroundSection.expanded - - RowLayout { - spacing: Appearance.spacing.smaller - - StyledText { - text: qsTr("Appearance") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - IconButton { - icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more" - type: IconButton.Text - label.animate: true - onClicked: { - const shouldExpand = !sidebarLayout.allSectionsExpanded; - themeModeSection.expanded = shouldExpand; - colorVariantSection.expanded = shouldExpand; - colorSchemeSection.expanded = shouldExpand; - animationsSection.expanded = shouldExpand; - fontsSection.expanded = shouldExpand; - scalesSection.expanded = shouldExpand; - transparencySection.expanded = shouldExpand; - borderSection.expanded = shouldExpand; - backgroundSection.expanded = shouldExpand; - } - } + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Appearance Settings") + font.pointSize: Appearance.font.size.large + font.bold: true } - CollapsibleSection { - id: themeModeSection - title: qsTr("Theme mode") - description: qsTr("Light or dark theme") - showBackground: true - - SwitchRow { - label: qsTr("Dark mode") - checked: !Colours.currentLight - onToggled: checked => { - Colours.setMode(checked ? "dark" : "light"); - } - } + StyledText { + Layout.topMargin: Appearance.spacing.large + Layout.alignment: Qt.AlignHCenter + text: qsTr("Wallpaper") + font.pointSize: Appearance.font.size.extraLarge + font.weight: 600 } - CollapsibleSection { - id: colorVariantSection - title: qsTr("Color variant") - description: qsTr("Material theme variant") - showBackground: true - - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small / 2 - - Repeater { - model: M3Variants.list - - delegate: StyledRect { - required property var modelData - - Layout.fillWidth: true - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - const variant = modelData.variant; - - // Optimistic update - set immediately for responsive UI - Schemes.currentVariant = variant; - Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Select a wallpaper") + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } - // Reload after a delay to confirm changes - Qt.callLater(() => { - reloadTimer.restart(); - }); + Item { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.large + Layout.preferredHeight: wallpaperLoader.item ? wallpaperLoader.item.layoutPreferredHeight : 0 + + Loader { + id: wallpaperLoader + anchors.fill: parent + asynchronous: true + active: { + // Lazy load: only activate when: + // 1. Right pane is loaded AND + // 2. Appearance pane is active (index 3) or adjacent (for smooth transitions) + // This prevents loading all wallpapers when control center opens but appearance pane isn't visible + const isActive = root.session.activeIndex === 3; + const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1; + // Access loader through SplitPaneLayout's rightLoader + const splitLayout = root.children[0]; + const loader = splitLayout && splitLayout.rightLoader ? splitLayout.rightLoader : null; + const shouldActivate = loader && loader.item !== null && (isActive || isAdjacent); + return shouldActivate; + } + + onStatusChanged: { + if (status === Loader.Error) { + console.error("[AppearancePane] Wallpaper loader error!"); + } + } + + // Stop lazy loading when loader becomes inactive + onActiveChanged: { + if (!active && wallpaperLoader.item) { + const container = wallpaperLoader.item; + // Access timer through wallpaperGrid + if (container && container.wallpaperGrid) { + const grid = container.wallpaperGrid; + if (grid.imageUpdateTimer) { + grid.imageUpdateTimer.stop(); } } - - Timer { - id: reloadTimer - interval: 300 - onTriggered: { - Schemes.reload(); + } + } + + sourceComponent: Item { + id: wallpaperGridContainer + property alias layoutPreferredHeight: wallpaperGrid.layoutPreferredHeight + + // Find and store reference to parent Flickable for scroll monitoring + property var parentFlickable: { + let item = parent; + while (item) { + if (item.flickableDirection !== undefined) { + return item; } + item = item.parent; } - - RowLayout { - id: variantRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - - MaterialIcon { - text: modelData.icon - font.pointSize: Appearance.font.size.large - fill: modelData.variant === Schemes.currentVariant ? 1 : 0 + return null; + } + + // Cleanup when component is destroyed + Component.onDestruction: { + if (wallpaperGrid) { + if (wallpaperGrid.scrollCheckTimer) { + wallpaperGrid.scrollCheckTimer.stop(); } - - StyledText { - Layout.fillWidth: true - text: modelData.name - font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 + wallpaperGrid._expansionInProgress = false; + } + } + + // Lazy loading model: loads one image at a time, only when touching bottom + // This prevents GridView from creating all delegates at once + QtObject { + id: lazyModel + + property var sourceList: null + property int loadedCount: 0 // Total items available to load + property int visibleCount: 0 // Items actually exposed to GridView (only visible + buffer) + property int totalCount: 0 + + function initialize(list) { + sourceList = list; + totalCount = list ? list.length : 0; + // Start with enough items to fill the initial viewport (~3 rows) + const initialRows = 3; + const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 3; + const initialCount = Math.min(initialRows * cols, totalCount); + loadedCount = initialCount; + visibleCount = initialCount; + } + + function loadOneRow() { + if (loadedCount < totalCount) { + const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 1; + const itemsToLoad = Math.min(cols, totalCount - loadedCount); + loadedCount += itemsToLoad; } - - MaterialIcon { - visible: modelData.variant === Schemes.currentVariant - text: "check" - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large + } + + function updateVisibleCount(neededCount) { + // Always round up to complete rows to avoid incomplete rows in the grid + const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 1; + const maxVisible = Math.min(neededCount, loadedCount); + const rows = Math.ceil(maxVisible / cols); + const newVisibleCount = Math.min(rows * cols, loadedCount); + + if (newVisibleCount > visibleCount) { + visibleCount = newVisibleCount; } } - - implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 } - } - } - } - - CollapsibleSection { - id: colorSchemeSection - title: qsTr("Color scheme") - description: qsTr("Available color schemes") - showBackground: true - - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small / 2 - - Repeater { - model: Schemes.list - - delegate: StyledRect { - required property var modelData - - Layout.fillWidth: true - - readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` - readonly property bool isCurrent: schemeKey === Schemes.currentScheme - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - const name = modelData.name; - const flavour = modelData.flavour; - const schemeKey = `${name} ${flavour}`; - - // Optimistic update - set immediately for responsive UI - Schemes.currentScheme = schemeKey; - Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - - // Reload after a delay to confirm changes - Qt.callLater(() => { - reloadTimer.restart(); - }); + + GridView { + id: wallpaperGrid + anchors.fill: parent + + property int _delegateCount: 0 + + readonly property int minCellWidth: 200 + Appearance.spacing.normal + readonly property int columnsCount: Math.max(1, Math.floor(parent.width / minCellWidth)) + + // Height based on visible items only - prevents GridView from creating all delegates + readonly property int layoutPreferredHeight: { + if (!lazyModel || lazyModel.visibleCount === 0 || columnsCount === 0) { + return 0; } + const calculated = Math.ceil(lazyModel.visibleCount / columnsCount) * cellHeight; + return calculated; } + + height: layoutPreferredHeight + cellWidth: width / columnsCount + cellHeight: 140 + Appearance.spacing.normal + + leftMargin: 0 + rightMargin: 0 + topMargin: 0 + bottomMargin: 0 + // Use ListModel for incremental updates to prevent flashing when new items are added + ListModel { + id: wallpaperListModel + } + + model: wallpaperListModel + + Connections { + target: lazyModel + function onVisibleCountChanged(): void { + if (!lazyModel || !lazyModel.sourceList) return; + + const newCount = lazyModel.visibleCount; + const currentCount = wallpaperListModel.count; + + // Only append new items - never remove or replace existing ones + if (newCount > currentCount) { + const flickable = wallpaperGridContainer.parentFlickable; + const oldScrollY = flickable ? flickable.contentY : 0; + + for (let i = currentCount; i < newCount; i++) { + wallpaperListModel.append({modelData: lazyModel.sourceList[i]}); + } + + // Preserve scroll position after model update + if (flickable) { + Qt.callLater(function() { + if (Math.abs(flickable.contentY - oldScrollY) < 1) { + flickable.contentY = oldScrollY; + } + }); + } + } + } + } + + Component.onCompleted: { + Qt.callLater(function() { + const isActive = root.session.activeIndex === 3; + if (width > 0 && parent && parent.visible && isActive && Wallpapers.list) { + lazyModel.initialize(Wallpapers.list); + wallpaperListModel.clear(); + for (let i = 0; i < lazyModel.visibleCount; i++) { + wallpaperListModel.append({modelData: lazyModel.sourceList[i]}); + } + } + }); + } + + Connections { + target: root.session + function onActiveIndexChanged(): void { + const isActive = root.session.activeIndex === 3; + + // Stop lazy loading when switching away from appearance pane + if (!isActive) { + if (scrollCheckTimer) { + scrollCheckTimer.stop(); + } + if (wallpaperGrid) { + wallpaperGrid._expansionInProgress = false; + } + return; + } + + // Initialize if needed when switching to appearance pane + if (isActive && width > 0 && !lazyModel.sourceList && parent && parent.visible && Wallpapers.list) { + lazyModel.initialize(Wallpapers.list); + wallpaperListModel.clear(); + for (let i = 0; i < lazyModel.visibleCount; i++) { + wallpaperListModel.append({modelData: lazyModel.sourceList[i]}); + } + } + } + } + + onWidthChanged: { + const isActive = root.session.activeIndex === 3; + if (width > 0 && !lazyModel.sourceList && parent && parent.visible && isActive && Wallpapers.list) { + lazyModel.initialize(Wallpapers.list); + wallpaperListModel.clear(); + for (let i = 0; i < lazyModel.visibleCount; i++) { + wallpaperListModel.append({modelData: lazyModel.sourceList[i]}); + } + } + } + + // Force true lazy loading: only create delegates for visible items + displayMarginBeginning: 0 + displayMarginEnd: 0 + cacheBuffer: 0 + + // Debounce expansion to avoid too frequent checks + property bool _expansionInProgress: false + + Connections { + target: wallpaperGridContainer.parentFlickable + function onContentYChanged(): void { + // Don't process scroll events if appearance pane is not active + const isActive = root.session.activeIndex === 3; + if (!isActive) return; + + if (!lazyModel || !lazyModel.sourceList || lazyModel.loadedCount >= lazyModel.totalCount || wallpaperGrid._expansionInProgress) { + return; + } + + const flickable = wallpaperGridContainer.parentFlickable; + if (!flickable) return; + + const gridY = wallpaperGridContainer.y; + const scrollY = flickable.contentY; + const viewportHeight = flickable.height; + + const topY = scrollY - gridY; + const bottomY = scrollY + viewportHeight - gridY; + + if (bottomY < 0) return; + + const topRow = Math.max(0, Math.floor(topY / wallpaperGrid.cellHeight)); + const bottomRow = Math.floor(bottomY / wallpaperGrid.cellHeight); + + // Update visible count with 1 row buffer ahead + const bufferRows = 1; + const neededBottomRow = bottomRow + bufferRows; + const neededCount = Math.min((neededBottomRow + 1) * wallpaperGrid.columnsCount, lazyModel.loadedCount); + lazyModel.updateVisibleCount(neededCount); + + // Load more when we're within 1 row of running out of loaded items + const loadedRows = Math.ceil(lazyModel.loadedCount / wallpaperGrid.columnsCount); + const rowsRemaining = loadedRows - (bottomRow + 1); + + if (rowsRemaining <= 1 && lazyModel.loadedCount < lazyModel.totalCount) { + if (!wallpaperGrid._expansionInProgress) { + wallpaperGrid._expansionInProgress = true; + lazyModel.loadOneRow(); + Qt.callLater(function() { + wallpaperGrid._expansionInProgress = false; + }); + } + } + } + } + + // Fallback timer to check scroll position periodically Timer { - id: reloadTimer - interval: 300 + id: scrollCheckTimer + interval: 100 + running: { + const isActive = root.session.activeIndex === 3; + return isActive && lazyModel && lazyModel.sourceList && lazyModel.loadedCount < lazyModel.totalCount; + } + repeat: true onTriggered: { - Schemes.reload(); + // Double-check that appearance pane is still active + const isActive = root.session.activeIndex === 3; + if (!isActive) { + stop(); + return; + } + + const flickable = wallpaperGridContainer.parentFlickable; + if (!flickable || !lazyModel || !lazyModel.sourceList) return; + + const gridY = wallpaperGridContainer.y; + const scrollY = flickable.contentY; + const viewportHeight = flickable.height; + + const topY = scrollY - gridY; + const bottomY = scrollY + viewportHeight - gridY; + if (bottomY < 0) return; + + const topRow = Math.max(0, Math.floor(topY / wallpaperGrid.cellHeight)); + const bottomRow = Math.floor(bottomY / wallpaperGrid.cellHeight); + + const bufferRows = 1; + const neededBottomRow = bottomRow + bufferRows; + const neededCount = Math.min((neededBottomRow + 1) * wallpaperGrid.columnsCount, lazyModel.loadedCount); + lazyModel.updateVisibleCount(neededCount); + + // Load more when we're within 1 row of running out of loaded items + const loadedRows = Math.ceil(lazyModel.loadedCount / wallpaperGrid.columnsCount); + const rowsRemaining = loadedRows - (bottomRow + 1); + + if (rowsRemaining <= 1 && lazyModel.loadedCount < lazyModel.totalCount) { + if (!wallpaperGrid._expansionInProgress) { + wallpaperGrid._expansionInProgress = true; + lazyModel.loadOneRow(); + Qt.callLater(function() { + wallpaperGrid._expansionInProgress = false; + }); + } + } } } + + + // Parent Flickable handles scrolling + interactive: false - RowLayout { - id: schemeRow - - anchors.fill: parent - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - StyledRect { - id: preview + delegate: Item { + required property var modelData - Layout.alignment: Qt.AlignVCenter + width: wallpaperGrid.cellWidth + height: wallpaperGrid.cellHeight - border.width: 1 - border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) + readonly property bool isCurrent: modelData.path === Wallpapers.actualCurrent + readonly property real itemMargin: Appearance.spacing.normal / 2 + readonly property real itemRadius: Appearance.rounding.normal + + Component.onCompleted: { + wallpaperGrid._delegateCount++; + } - color: `#${modelData.colours?.surface}` - radius: Appearance.rounding.full - implicitWidth: iconPlaceholder.implicitWidth - implicitHeight: iconPlaceholder.implicitWidth + StateLayer { + anchors.fill: parent + anchors.leftMargin: itemMargin + anchors.rightMargin: itemMargin + anchors.topMargin: itemMargin + anchors.bottomMargin: itemMargin + radius: itemRadius - MaterialIcon { - id: iconPlaceholder - visible: false - text: "circle" - font.pointSize: Appearance.font.size.large - } + function onClicked(): void { + Wallpapers.setWallpaper(modelData.path); + } + } - Item { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right + StyledClippingRect { + id: image - implicitWidth: parent.implicitWidth / 2 - clip: true + anchors.fill: parent + anchors.leftMargin: itemMargin + anchors.rightMargin: itemMargin + anchors.topMargin: itemMargin + anchors.bottomMargin: itemMargin + color: Colours.tPalette.m3surfaceContainer + radius: itemRadius + antialiasing: true + layer.enabled: true + layer.smooth: true - StyledRect { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right + CachingImage { + id: cachingImage - implicitWidth: preview.implicitWidth - color: `#${modelData.colours?.primary}` - radius: Appearance.rounding.full - } - } - } + path: modelData.path + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + cache: true + visible: opacity > 0 + antialiasing: true + smooth: true - Column { - Layout.fillWidth: true - spacing: 0 + opacity: status === Image.Ready ? 1 : 0 - StyledText { - text: modelData.flavour ?? "" - font.pointSize: Appearance.font.size.normal - } + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutQuad + } + } + } - StyledText { - text: modelData.name ?? "" - font.pointSize: Appearance.font.size.small - color: Colours.palette.m3outline + // Fallback if CachingImage fails to load + Image { + id: fallbackImage - elide: Text.ElideRight - anchors.left: parent.left - anchors.right: parent.right - } - } + anchors.fill: parent + source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : "" + asynchronous: true + fillMode: Image.PreserveAspectCrop + cache: true + visible: opacity > 0 + antialiasing: true + smooth: true - Loader { - active: isCurrent - asynchronous: true + opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutQuad } } + } - implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 + Timer { + id: fallbackTimer + + property bool triggered: false + interval: 800 + running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null + onTriggered: triggered = true } - } - } - } - CollapsibleSection { - id: animationsSection - title: qsTr("Animations") - showBackground: true + // Gradient overlay for filename + Rectangle { + id: filenameOverlay - SectionContainer { - contentSpacing: Appearance.spacing.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5 + radius: 0 + + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.rgba(Colours.palette.m3surfaceContainer.r, + Colours.palette.m3surfaceContainer.g, + Colours.palette.m3surfaceContainer.b, 0) + } + GradientStop { + position: 0.3 + color: Qt.rgba(Colours.palette.m3surfaceContainer.r, + Colours.palette.m3surfaceContainer.g, + Colours.palette.m3surfaceContainer.b, 0.7) + } + GradientStop { + position: 0.6 + color: Qt.rgba(Colours.palette.m3surfaceContainer.r, + Colours.palette.m3surfaceContainer.g, + Colours.palette.m3surfaceContainer.b, 0.9) + } + GradientStop { + position: 1.0 + color: Qt.rgba(Colours.palette.m3surfaceContainer.r, + Colours.palette.m3surfaceContainer.g, + Colours.palette.m3surfaceContainer.b, 0.95) + } + } - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + opacity: 0 - StyledText { - text: qsTr("Animation duration scale") - font.pointSize: Appearance.font.size.normal + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic + } } - Item { - Layout.fillWidth: true + Component.onCompleted: { + opacity = 1; } + } + } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: animDurationsInput.implicitHeight + Appearance.padding.small * 2 - color: animDurationsInputHover.containsMouse || animDurationsInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: animDurationsInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) + Rectangle { + anchors.fill: parent + anchors.leftMargin: itemMargin + anchors.rightMargin: itemMargin + anchors.topMargin: itemMargin + anchors.bottomMargin: itemMargin + color: "transparent" + radius: itemRadius + border.width + border.width: isCurrent ? 2 : 0 + border.color: Colours.palette.m3primary + antialiasing: true + smooth: true - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } + Behavior on border.width { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } - MouseArea { - id: animDurationsInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } + MaterialIcon { + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Appearance.padding.small - StyledTextField { - id: animDurationsInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 5.0 } - - Component.onCompleted: { - text = (rootPane.animDurationsScale).toFixed(1); - } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 5.0) { - rootPane.animDurationsScale = val; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 5.0) { - text = (rootPane.animDurationsScale).toFixed(1); - } - } - } - } + visible: isCurrent + text: "check_circle" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.large + } + } - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } + StyledText { + id: filenameText + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 + anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 + anchors.bottomMargin: Appearance.padding.normal + + readonly property string fileName: { + const path = modelData.relativePath || ""; + const parts = path.split("/"); + return parts.length > 0 ? parts[parts.length - 1] : path; } - StyledSlider { - id: animDurationsSlider + text: fileName + font.pointSize: Appearance.font.size.smaller + font.weight: 500 + color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface + elide: Text.ElideMiddle + maximumLineCount: 1 + horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + opacity: 0 - from: 0.1 - to: 5.0 - value: rootPane.animDurationsScale - onMoved: { - rootPane.animDurationsScale = animDurationsSlider.value; - if (!animDurationsInput.activeFocus) { - animDurationsInput.text = (animDurationsSlider.value).toFixed(1); - } - rootPane.saveConfig(); + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic } } + + Component.onCompleted: { + opacity = 1; + } + } + } + } } } } + } + } + } - CollapsibleSection { - id: fontsSection - title: qsTr("Fonts") - showBackground: true - - CollapsibleSection { - id: materialFontSection - title: qsTr("Material font family") - expanded: true - showBackground: true - nested: true - - Loader { - id: materialFontLoader - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: materialFontSection.expanded - - sourceComponent: StyledListView { - id: materialFontList - property alias contentHeight: materialFontList.contentHeight - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() - - StyledScrollBar.vertical: StyledScrollBar { - flickable: materialFontList - } - - delegate: StyledRect { - required property string modelData - required property int index + SplitPaneLayout { + anchors.fill: parent - width: ListView.view.width + leftContent: Component { - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + StyledFlickable { + id: sidebarFlickable + readonly property var rootPane: root + flickableDirection: Flickable.VerticalFlick + contentHeight: sidebarLayout.height - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMaterial = modelData; - rootPane.saveConfig(); - } - } - RowLayout { - id: fontFamilyMaterialRow + StyledScrollBar.vertical: StyledScrollBar { + flickable: sidebarFlickable + } - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + ColumnLayout { + id: sidebarLayout + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small - spacing: Appearance.spacing.normal + readonly property bool allSectionsExpanded: + themeModeSection.expanded && + colorVariantSection.expanded && + colorSchemeSection.expanded && + animationsSection.expanded && + fontsSection.expanded && + scalesSection.expanded && + transparencySection.expanded && + borderSection.expanded && + backgroundSection.expanded - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + RowLayout { + spacing: Appearance.spacing.smaller - Item { - Layout.fillWidth: true - } + StyledText { + text: qsTr("Appearance") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } - Loader { - active: isCurrent - asynchronous: true + Item { + Layout.fillWidth: true + } - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } - } - } + IconButton { + icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more" + type: IconButton.Text + label.animate: true + onClicked: { + const shouldExpand = !sidebarLayout.allSectionsExpanded; + themeModeSection.expanded = shouldExpand; + colorVariantSection.expanded = shouldExpand; + colorSchemeSection.expanded = shouldExpand; + animationsSection.expanded = shouldExpand; + fontsSection.expanded = shouldExpand; + scalesSection.expanded = shouldExpand; + transparencySection.expanded = shouldExpand; + borderSection.expanded = shouldExpand; + backgroundSection.expanded = shouldExpand; + } + } + } - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 - } - } + CollapsibleSection { + id: themeModeSection + title: qsTr("Theme mode") + description: qsTr("Light or dark theme") + showBackground: true + SwitchRow { + label: qsTr("Dark mode") + checked: !Colours.currentLight + onToggled: checked => { + Colours.setMode(checked ? "dark" : "light"); } } + } - CollapsibleSection { - id: monoFontSection - title: qsTr("Monospace font family") - expanded: false - showBackground: true - nested: true + CollapsibleSection { + id: colorVariantSection + title: qsTr("Color variant") + description: qsTr("Material theme variant") + showBackground: true - Loader { - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: monoFontSection.expanded + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small / 2 - sourceComponent: StyledListView { - id: monoFontList - property alias contentHeight: monoFontList.contentHeight + Repeater { + model: M3Variants.list - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() + delegate: StyledRect { + required property var modelData - StyledScrollBar.vertical: StyledScrollBar { - flickable: monoFontList - } + Layout.fillWidth: true - delegate: StyledRect { - required property string modelData - required property int index + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 + border.color: Colours.palette.m3primary - width: ListView.view.width + StateLayer { + function onClicked(): void { + const variant = modelData.variant; - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + // Optimistic update - set immediately for responsive UI + Schemes.currentVariant = variant; + Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMono = modelData; - rootPane.saveConfig(); - } + // Reload after a delay to confirm changes + Qt.callLater(() => { + reloadTimer.restart(); + }); + } + } + + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); } + } - RowLayout { - id: fontFamilyMonoRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal + RowLayout { + id: variantRow - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - Item { - Layout.fillWidth: true - } + spacing: Appearance.spacing.normal - Loader { - active: isCurrent - asynchronous: true + MaterialIcon { + text: modelData.icon + font.pointSize: Appearance.font.size.large + fill: modelData.variant === Schemes.currentVariant ? 1 : 0 + } - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } - } + StyledText { + Layout.fillWidth: true + text: modelData.name + font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 } - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + MaterialIcon { + visible: modelData.variant === Schemes.currentVariant + text: "check" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.large + } } + + implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 } } } + } - CollapsibleSection { - id: sansFontSection - title: qsTr("Sans-serif font family") - expanded: false - showBackground: true - nested: true - - Loader { - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: sansFontSection.expanded - - sourceComponent: StyledListView { - id: sansFontList - property alias contentHeight: sansFontList.contentHeight - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() + CollapsibleSection { + id: colorSchemeSection + title: qsTr("Color scheme") + description: qsTr("Available color schemes") + showBackground: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: sansFontList - } + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small / 2 - delegate: StyledRect { - required property string modelData - required property int index + Repeater { + model: Schemes.list - width: ListView.view.width + delegate: StyledRect { + required property var modelData - readonly property bool isCurrent: modelData === rootPane.fontFamilySans - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary + Layout.fillWidth: true - StateLayer { - function onClicked(): void { - rootPane.fontFamilySans = modelData; - rootPane.saveConfig(); - } - } + readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` + readonly property bool isCurrent: schemeKey === Schemes.currentScheme - RowLayout { - id: fontFamilySansRow + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + StateLayer { + function onClicked(): void { + const name = modelData.name; + const flavour = modelData.flavour; + const schemeKey = `${name} ${flavour}`; - spacing: Appearance.spacing.normal + // Optimistic update - set immediately for responsive UI + Schemes.currentScheme = schemeKey; + Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } + // Reload after a delay to confirm changes + Qt.callLater(() => { + reloadTimer.restart(); + }); + } + } - Item { - Layout.fillWidth: true - } + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); + } + } - Loader { - active: isCurrent - asynchronous: true + RowLayout { + id: schemeRow - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } - } - } + anchors.fill: parent + anchors.margins: Appearance.padding.normal - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 - } - } - } - } + spacing: Appearance.spacing.normal - SectionContainer { - contentSpacing: Appearance.spacing.normal + StyledRect { + id: preview - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + Layout.alignment: Qt.AlignVCenter - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + border.width: 1 + border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) - StyledText { - text: qsTr("Font size scale") - font.pointSize: Appearance.font.size.normal - } + color: `#${modelData.colours?.surface}` + radius: Appearance.rounding.full + implicitWidth: iconPlaceholder.implicitWidth + implicitHeight: iconPlaceholder.implicitWidth - Item { - Layout.fillWidth: true - } + MaterialIcon { + id: iconPlaceholder + visible: false + text: "circle" + font.pointSize: Appearance.font.size.large + } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: fontSizeInput.implicitHeight + Appearance.padding.small * 2 - color: fontSizeInputHover.containsMouse || fontSizeInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: fontSizeInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) + Item { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } + implicitWidth: parent.implicitWidth / 2 + clip: true - MouseArea { - id: fontSizeInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right - StyledTextField { - id: fontSizeInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.7; top: 1.5 } - - Component.onCompleted: { - text = (rootPane.fontSizeScale).toFixed(1); - } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.7 && val <= 1.5) { - rootPane.fontSizeScale = val; - rootPane.saveConfig(); - } + implicitWidth: preview.implicitWidth + color: `#${modelData.colours?.primary}` + radius: Appearance.rounding.full } } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.7 || val > 1.5) { - text = (rootPane.fontSizeScale).toFixed(1); - } + } + + Column { + Layout.fillWidth: true + spacing: 0 + + StyledText { + text: modelData.flavour ?? "" + font.pointSize: Appearance.font.size.normal } - } - } - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } + StyledText { + text: modelData.name ?? "" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline - StyledSlider { - id: fontSizeSlider + elide: Text.ElideRight + anchors.left: parent.left + anchors.right: parent.right + } + } - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + Loader { + active: isCurrent + asynchronous: true - from: 0.7 - to: 1.5 - value: rootPane.fontSizeScale - onMoved: { - rootPane.fontSizeScale = fontSizeSlider.value; - if (!fontSizeInput.activeFocus) { - fontSizeInput.text = (fontSizeSlider.value).toFixed(1); + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } } - rootPane.saveConfig(); } + + implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 } } } } CollapsibleSection { - id: scalesSection - title: qsTr("Scales") + id: animationsSection + title: qsTr("Animations") showBackground: true SectionContainer { @@ -881,7 +1005,7 @@ RowLayout { spacing: Appearance.spacing.normal StyledText { - text: qsTr("Padding scale") + text: qsTr("Animation duration scale") font.pointSize: Appearance.font.size.normal } @@ -891,13 +1015,13 @@ RowLayout { StyledRect { Layout.preferredWidth: 70 - implicitHeight: paddingInput.implicitHeight + Appearance.padding.small * 2 - color: paddingInputHover.containsMouse || paddingInput.activeFocus + implicitHeight: animDurationsInput.implicitHeight + Appearance.padding.small * 2 + color: animDurationsInputHover.containsMouse || animDurationsInput.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 - border.color: paddingInput.activeFocus + border.color: animDurationsInput.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -905,7 +1029,7 @@ RowLayout { Behavior on border.color { CAnim {} } MouseArea { - id: paddingInputHover + id: animDurationsInputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -913,29 +1037,29 @@ RowLayout { } StyledTextField { - id: paddingInput + id: animDurationsInput anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.5; top: 2.0 } + validator: DoubleValidator { bottom: 0.1; top: 5.0 } Component.onCompleted: { - text = (rootPane.paddingScale).toFixed(1); + text = (rootPane.animDurationsScale).toFixed(1); } onTextChanged: { if (activeFocus) { const val = parseFloat(text); - if (!isNaN(val) && val >= 0.5 && val <= 2.0) { - rootPane.paddingScale = val; + if (!isNaN(val) && val >= 0.1 && val <= 5.0) { + rootPane.animDurationsScale = val; rootPane.saveConfig(); } } } onEditingFinished: { const val = parseFloat(text); - if (isNaN(val) || val < 0.5 || val > 2.0) { - text = (rootPane.paddingScale).toFixed(1); + if (isNaN(val) || val < 0.1 || val > 5.0) { + text = (rootPane.animDurationsScale).toFixed(1); } } } @@ -949,332 +1073,271 @@ RowLayout { } StyledSlider { - id: paddingSlider + id: animDurationsSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 - from: 0.5 - to: 2.0 - value: rootPane.paddingScale + from: 0.1 + to: 5.0 + value: rootPane.animDurationsScale onMoved: { - rootPane.paddingScale = paddingSlider.value; - if (!paddingInput.activeFocus) { - paddingInput.text = (paddingSlider.value).toFixed(1); + rootPane.animDurationsScale = animDurationsSlider.value; + if (!animDurationsInput.activeFocus) { + animDurationsInput.text = (animDurationsSlider.value).toFixed(1); } rootPane.saveConfig(); } } } } + } - SectionContainer { - contentSpacing: Appearance.spacing.normal + CollapsibleSection { + id: fontsSection + title: qsTr("Fonts") + showBackground: true - ColumnLayout { + CollapsibleSection { + id: materialFontSection + title: qsTr("Material font family") + expanded: true + showBackground: true + nested: true + + Loader { + id: materialFontLoader Layout.fillWidth: true - spacing: Appearance.spacing.small + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: materialFontSection.expanded - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + sourceComponent: StyledListView { + id: materialFontList + property alias contentHeight: materialFontList.contentHeight - StyledText { - text: qsTr("Rounding scale") - font.pointSize: Appearance.font.size.normal - } + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() - Item { - Layout.fillWidth: true + StyledScrollBar.vertical: StyledScrollBar { + flickable: materialFontList } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: roundingInput.implicitHeight + Appearance.padding.small * 2 - color: roundingInputHover.containsMouse || roundingInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: roundingInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) + delegate: StyledRect { + required property string modelData + required property int index - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } + width: ListView.view.width - MouseArea { - id: roundingInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMaterial = modelData; + rootPane.saveConfig(); + } } - StyledTextField { - id: roundingInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 5.0 } - - Component.onCompleted: { - text = (rootPane.roundingScale).toFixed(1); + RowLayout { + id: fontFamilyMaterialRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 5.0) { - rootPane.roundingScale = val; - rootPane.saveConfig(); - } - } + + Item { + Layout.fillWidth: true } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 5.0) { - text = (rootPane.roundingScale).toFixed(1); + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large } } } - } - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 } } - StyledSlider { - id: roundingSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0.1 - to: 5.0 - value: rootPane.roundingScale - onMoved: { - rootPane.roundingScale = roundingSlider.value; - if (!roundingInput.activeFocus) { - roundingInput.text = (roundingSlider.value).toFixed(1); - } - rootPane.saveConfig(); - } - } } } - SectionContainer { - contentSpacing: Appearance.spacing.normal + CollapsibleSection { + id: monoFontSection + title: qsTr("Monospace font family") + expanded: false + showBackground: true + nested: true - ColumnLayout { + Loader { Layout.fillWidth: true - spacing: Appearance.spacing.small + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: monoFontSection.expanded - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + sourceComponent: StyledListView { + id: monoFontList + property alias contentHeight: monoFontList.contentHeight - StyledText { - text: qsTr("Spacing scale") - font.pointSize: Appearance.font.size.normal + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() + + StyledScrollBar.vertical: StyledScrollBar { + flickable: monoFontList } - Item { - Layout.fillWidth: true - } + delegate: StyledRect { + required property string modelData + required property int index - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: spacingInput.implicitHeight + Appearance.padding.small * 2 - color: spacingInputHover.containsMouse || spacingInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: spacingInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) + width: ListView.view.width - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - MouseArea { - id: spacingInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMono = modelData; + rootPane.saveConfig(); + } } - StyledTextField { - id: spacingInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 2.0 } - - Component.onCompleted: { - text = (rootPane.spacingScale).toFixed(1); + RowLayout { + id: fontFamilyMonoRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 2.0) { - rootPane.spacingScale = val; - rootPane.saveConfig(); - } - } + + Item { + Layout.fillWidth: true } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 2.0) { - text = (rootPane.spacingScale).toFixed(1); + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large } } } - } - - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } - - StyledSlider { - id: spacingSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - from: 0.1 - to: 2.0 - value: rootPane.spacingScale - onMoved: { - rootPane.spacingScale = spacingSlider.value; - if (!spacingInput.activeFocus) { - spacingInput.text = (spacingSlider.value).toFixed(1); - } - rootPane.saveConfig(); + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 } } } } - } - - CollapsibleSection { - id: transparencySection - title: qsTr("Transparency") - showBackground: true - - SwitchRow { - label: qsTr("Transparency enabled") - checked: rootPane.transparencyEnabled - onToggled: checked => { - rootPane.transparencyEnabled = checked; - rootPane.saveConfig(); - } - } - SectionContainer { - contentSpacing: Appearance.spacing.normal + CollapsibleSection { + id: sansFontSection + title: qsTr("Sans-serif font family") + expanded: false + showBackground: true + nested: true - ColumnLayout { + Loader { Layout.fillWidth: true - spacing: Appearance.spacing.small + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: sansFontSection.expanded - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + sourceComponent: StyledListView { + id: sansFontList + property alias contentHeight: sansFontList.contentHeight - StyledText { - text: qsTr("Transparency base") - font.pointSize: Appearance.font.size.normal - } + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() - Item { - Layout.fillWidth: true + StyledScrollBar.vertical: StyledScrollBar { + flickable: sansFontList } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: transparencyBaseInput.implicitHeight + Appearance.padding.small * 2 - color: transparencyBaseInputHover.containsMouse || transparencyBaseInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: transparencyBaseInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) + delegate: StyledRect { + required property string modelData + required property int index - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } + width: ListView.view.width - MouseArea { - id: transparencyBaseInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } + readonly property bool isCurrent: modelData === rootPane.fontFamilySans + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary - StyledTextField { - id: transparencyBaseInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } - - Component.onCompleted: { - text = Math.round(rootPane.transparencyBase * 100).toString(); - } - - onTextChanged: { - if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - rootPane.transparencyBase = val / 100; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(rootPane.transparencyBase * 100).toString(); - } + StateLayer { + function onClicked(): void { + rootPane.fontFamilySans = modelData; + rootPane.saveConfig(); } } - } - StyledText { - text: "%" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } + RowLayout { + id: fontFamilySansRow - StyledSlider { - id: baseSlider + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + spacing: Appearance.spacing.normal - from: 0 - to: 100 - value: rootPane.transparencyBase * 100 - onMoved: { - rootPane.transparencyBase = baseSlider.value / 100; - if (!transparencyBaseInput.activeFocus) { - transparencyBaseInput.text = Math.round(baseSlider.value).toString(); + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } } - rootPane.saveConfig(); + + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 } } } @@ -1292,7 +1355,7 @@ RowLayout { spacing: Appearance.spacing.normal StyledText { - text: qsTr("Transparency layers") + text: qsTr("Font size scale") font.pointSize: Appearance.font.size.normal } @@ -1302,13 +1365,13 @@ RowLayout { StyledRect { Layout.preferredWidth: 70 - implicitHeight: transparencyLayersInput.implicitHeight + Appearance.padding.small * 2 - color: transparencyLayersInputHover.containsMouse || transparencyLayersInput.activeFocus + implicitHeight: fontSizeInput.implicitHeight + Appearance.padding.small * 2 + color: fontSizeInputHover.containsMouse || fontSizeInput.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 - border.color: transparencyLayersInput.activeFocus + border.color: fontSizeInput.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -1316,7 +1379,7 @@ RowLayout { Behavior on border.color { CAnim {} } MouseArea { - id: transparencyLayersInputHover + id: fontSizeInputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -1324,56 +1387,56 @@ RowLayout { } StyledTextField { - id: transparencyLayersInput + id: fontSizeInput anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } + validator: DoubleValidator { bottom: 0.7; top: 1.5 } Component.onCompleted: { - text = Math.round(rootPane.transparencyLayers * 100).toString(); + text = (rootPane.fontSizeScale).toFixed(1); } onTextChanged: { if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - rootPane.transparencyLayers = val / 100; + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.7 && val <= 1.5) { + rootPane.fontSizeScale = val; rootPane.saveConfig(); } } } onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(rootPane.transparencyLayers * 100).toString(); + const val = parseFloat(text); + if (isNaN(val) || val < 0.7 || val > 1.5) { + text = (rootPane.fontSizeScale).toFixed(1); } } } } StyledText { - text: "%" + text: "×" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal } } StyledSlider { - id: layersSlider + id: fontSizeSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 - from: 0 - to: 100 - value: rootPane.transparencyLayers * 100 + from: 0.7 + to: 1.5 + value: rootPane.fontSizeScale onMoved: { - rootPane.transparencyLayers = layersSlider.value / 100; - if (!transparencyLayersInput.activeFocus) { - transparencyLayersInput.text = Math.round(layersSlider.value).toString(); + rootPane.fontSizeScale = fontSizeSlider.value; + if (!fontSizeInput.activeFocus) { + fontSizeInput.text = (fontSizeSlider.value).toFixed(1); } - rootPane.saveConfig(); + rootPane.saveConfig(); } } } @@ -1381,8 +1444,8 @@ RowLayout { } CollapsibleSection { - id: borderSection - title: qsTr("Border") + id: scalesSection + title: qsTr("Scales") showBackground: true SectionContainer { @@ -1397,7 +1460,7 @@ RowLayout { spacing: Appearance.spacing.normal StyledText { - text: qsTr("Border rounding") + text: qsTr("Padding scale") font.pointSize: Appearance.font.size.normal } @@ -1407,13 +1470,13 @@ RowLayout { StyledRect { Layout.preferredWidth: 70 - implicitHeight: borderRoundingInput.implicitHeight + Appearance.padding.small * 2 - color: borderRoundingInputHover.containsMouse || borderRoundingInput.activeFocus + implicitHeight: paddingInput.implicitHeight + Appearance.padding.small * 2 + color: paddingInputHover.containsMouse || paddingInput.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 - border.color: borderRoundingInput.activeFocus + border.color: paddingInput.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -1421,7 +1484,7 @@ RowLayout { Behavior on border.color { CAnim {} } MouseArea { - id: borderRoundingInputHover + id: paddingInputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -1429,48 +1492,54 @@ RowLayout { } StyledTextField { - id: borderRoundingInput + id: paddingInput anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 100 } + validator: DoubleValidator { bottom: 0.5; top: 2.0 } Component.onCompleted: { - text = (rootPane.borderRounding).toFixed(1); + text = (rootPane.paddingScale).toFixed(1); } onTextChanged: { if (activeFocus) { const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 100) { - rootPane.borderRounding = val; + if (!isNaN(val) && val >= 0.5 && val <= 2.0) { + rootPane.paddingScale = val; rootPane.saveConfig(); } } } onEditingFinished: { const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 100) { - text = (rootPane.borderRounding).toFixed(1); + if (isNaN(val) || val < 0.5 || val > 2.0) { + text = (rootPane.paddingScale).toFixed(1); } } } } + + StyledText { + text: "×" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } } StyledSlider { - id: borderRoundingSlider + id: paddingSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 - from: 0.1 - to: 100 - value: rootPane.borderRounding + from: 0.5 + to: 2.0 + value: rootPane.paddingScale onMoved: { - rootPane.borderRounding = borderRoundingSlider.value; - if (!borderRoundingInput.activeFocus) { - borderRoundingInput.text = (borderRoundingSlider.value).toFixed(1); + rootPane.paddingScale = paddingSlider.value; + if (!paddingInput.activeFocus) { + paddingInput.text = (paddingSlider.value).toFixed(1); } rootPane.saveConfig(); } @@ -1490,7 +1559,7 @@ RowLayout { spacing: Appearance.spacing.normal StyledText { - text: qsTr("Border thickness") + text: qsTr("Rounding scale") font.pointSize: Appearance.font.size.normal } @@ -1500,13 +1569,13 @@ RowLayout { StyledRect { Layout.preferredWidth: 70 - implicitHeight: borderThicknessInput.implicitHeight + Appearance.padding.small * 2 - color: borderThicknessInputHover.containsMouse || borderThicknessInput.activeFocus + implicitHeight: roundingInput.implicitHeight + Appearance.padding.small * 2 + color: roundingInputHover.containsMouse || roundingInput.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 - border.color: borderThicknessInput.activeFocus + border.color: roundingInput.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -1514,7 +1583,7 @@ RowLayout { Behavior on border.color { CAnim {} } MouseArea { - id: borderThicknessInputHover + id: roundingInputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -1522,103 +1591,60 @@ RowLayout { } StyledTextField { - id: borderThicknessInput + id: roundingInput anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 100 } + validator: DoubleValidator { bottom: 0.1; top: 5.0 } Component.onCompleted: { - text = (rootPane.borderThickness).toFixed(1); + text = (rootPane.roundingScale).toFixed(1); } onTextChanged: { if (activeFocus) { const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 100) { - rootPane.borderThickness = val; + if (!isNaN(val) && val >= 0.1 && val <= 5.0) { + rootPane.roundingScale = val; rootPane.saveConfig(); } } } onEditingFinished: { const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 100) { - text = (rootPane.borderThickness).toFixed(1); + if (isNaN(val) || val < 0.1 || val > 5.0) { + text = (rootPane.roundingScale).toFixed(1); } } } } + + StyledText { + text: "×" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } } StyledSlider { - id: borderThicknessSlider + id: roundingSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 from: 0.1 - to: 100 - value: rootPane.borderThickness + to: 5.0 + value: rootPane.roundingScale onMoved: { - rootPane.borderThickness = borderThicknessSlider.value; - if (!borderThicknessInput.activeFocus) { - borderThicknessInput.text = (borderThicknessSlider.value).toFixed(1); + rootPane.roundingScale = roundingSlider.value; + if (!roundingInput.activeFocus) { + roundingInput.text = (roundingSlider.value).toFixed(1); } rootPane.saveConfig(); } } } } - } - - CollapsibleSection { - id: backgroundSection - title: qsTr("Background") - showBackground: true - - SwitchRow { - label: qsTr("Desktop clock") - checked: rootPane.desktopClockEnabled - onToggled: checked => { - rootPane.desktopClockEnabled = checked; - rootPane.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Background enabled") - checked: rootPane.backgroundEnabled - onToggled: checked => { - rootPane.backgroundEnabled = checked; - rootPane.saveConfig(); - } - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Visualiser") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - SwitchRow { - label: qsTr("Visualiser enabled") - checked: rootPane.visualiserEnabled - onToggled: checked => { - rootPane.visualiserEnabled = checked; - rootPane.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Visualiser auto hide") - checked: rootPane.visualiserAutoHide - onToggled: checked => { - rootPane.visualiserAutoHide = checked; - rootPane.saveConfig(); - } - } SectionContainer { contentSpacing: Appearance.spacing.normal @@ -1632,7 +1658,7 @@ RowLayout { spacing: Appearance.spacing.normal StyledText { - text: qsTr("Visualiser rounding") + text: qsTr("Spacing scale") font.pointSize: Appearance.font.size.normal } @@ -1642,13 +1668,13 @@ RowLayout { StyledRect { Layout.preferredWidth: 70 - implicitHeight: visualiserRoundingInput.implicitHeight + Appearance.padding.small * 2 - color: visualiserRoundingInputHover.containsMouse || visualiserRoundingInput.activeFocus + implicitHeight: spacingInput.implicitHeight + Appearance.padding.small * 2 + color: spacingInputHover.containsMouse || spacingInput.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 - border.color: visualiserRoundingInput.activeFocus + border.color: spacingInput.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -1656,7 +1682,7 @@ RowLayout { Behavior on border.color { CAnim {} } MouseArea { - id: visualiserRoundingInputHover + id: spacingInputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -1664,55 +1690,75 @@ RowLayout { } StyledTextField { - id: visualiserRoundingInput + id: spacingInput anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 10 } + validator: DoubleValidator { bottom: 0.1; top: 2.0 } Component.onCompleted: { - text = Math.round(rootPane.visualiserRounding).toString(); + text = (rootPane.spacingScale).toFixed(1); } onTextChanged: { if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 10) { - rootPane.visualiserRounding = val; - rootPane.saveConfig(); + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 2.0) { + rootPane.spacingScale = val; + rootPane.saveConfig(); } } } onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 10) { - text = Math.round(rootPane.visualiserRounding).toString(); + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 2.0) { + text = (rootPane.spacingScale).toFixed(1); } } } } + + StyledText { + text: "×" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } } StyledSlider { - id: visualiserRoundingSlider + id: spacingSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 - from: 0 - to: 10 - stepSize: 1 - value: rootPane.visualiserRounding + from: 0.1 + to: 2.0 + value: rootPane.spacingScale onMoved: { - rootPane.visualiserRounding = Math.round(visualiserRoundingSlider.value); - if (!visualiserRoundingInput.activeFocus) { - visualiserRoundingInput.text = Math.round(visualiserRoundingSlider.value).toString(); + rootPane.spacingScale = spacingSlider.value; + if (!spacingInput.activeFocus) { + spacingInput.text = (spacingSlider.value).toFixed(1); } rootPane.saveConfig(); + } + } } } } - } + + CollapsibleSection { + id: transparencySection + title: qsTr("Transparency") + showBackground: true + + SwitchRow { + label: qsTr("Transparency enabled") + checked: rootPane.transparencyEnabled + onToggled: checked => { + rootPane.transparencyEnabled = checked; + rootPane.saveConfig(); + } + } SectionContainer { contentSpacing: Appearance.spacing.normal @@ -1726,7 +1772,7 @@ RowLayout { spacing: Appearance.spacing.normal StyledText { - text: qsTr("Visualiser spacing") + text: qsTr("Transparency base") font.pointSize: Appearance.font.size.normal } @@ -1736,13 +1782,13 @@ RowLayout { StyledRect { Layout.preferredWidth: 70 - implicitHeight: visualiserSpacingInput.implicitHeight + Appearance.padding.small * 2 - color: visualiserSpacingInputHover.containsMouse || visualiserSpacingInput.activeFocus + implicitHeight: transparencyBaseInput.implicitHeight + Appearance.padding.small * 2 + color: transparencyBaseInputHover.containsMouse || transparencyBaseInput.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 - border.color: visualiserSpacingInput.activeFocus + border.color: transparencyBaseInput.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -1750,7 +1796,7 @@ RowLayout { Behavior on border.color { CAnim {} } MouseArea { - id: visualiserSpacingInputHover + id: transparencyBaseInputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -1758,701 +1804,592 @@ RowLayout { } StyledTextField { - id: visualiserSpacingInput + id: transparencyBaseInput anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0; top: 2 } + validator: IntValidator { bottom: 0; top: 100 } Component.onCompleted: { - text = (rootPane.visualiserSpacing).toFixed(1); + text = Math.round(rootPane.transparencyBase * 100).toString(); } onTextChanged: { if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0 && val <= 2) { - rootPane.visualiserSpacing = val; + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + rootPane.transparencyBase = val / 100; rootPane.saveConfig(); } } } onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0 || val > 2) { - text = (rootPane.visualiserSpacing).toFixed(1); + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(rootPane.transparencyBase * 100).toString(); } } } } + + StyledText { + text: "%" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } } StyledSlider { - id: visualiserSpacingSlider + id: baseSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 from: 0 - to: 2 - value: rootPane.visualiserSpacing + to: 100 + value: rootPane.transparencyBase * 100 onMoved: { - rootPane.visualiserSpacing = visualiserSpacingSlider.value; - if (!visualiserSpacingInput.activeFocus) { - visualiserSpacingInput.text = (visualiserSpacingSlider.value).toFixed(1); + rootPane.transparencyBase = baseSlider.value / 100; + if (!transparencyBaseInput.activeFocus) { + transparencyBaseInput.text = Math.round(baseSlider.value).toString(); } rootPane.saveConfig(); } } } } - } - } - } - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - id: rightAppearanceClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - radius: rightAppearanceBorder.innerRadius - color: "transparent" - - Loader { - id: rightAppearanceLoader - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - asynchronous: true - sourceComponent: appearanceRightContentComponent - property var rootPane: root - - onStatusChanged: { - if (status === Loader.Error) { - console.error("[AppearancePane] Right appearance loader error!"); - } - } - } - } - - InnerBorder { - id: rightAppearanceBorder - leftThickness: Appearance.padding.normal / 2 - } - - Component { - id: appearanceRightContentComponent - - StyledFlickable { - id: rightAppearanceFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: rightAppearanceFlickable - } - ColumnLayout { - id: contentLayout + SectionContainer { + contentSpacing: Appearance.spacing.normal - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: Appearance.spacing.normal + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - MaterialIcon { - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.topMargin: 0 - text: "palette" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Appearance Settings") - font.pointSize: Appearance.font.size.large - font.bold: true - } + StyledText { + text: qsTr("Transparency layers") + font.pointSize: Appearance.font.size.normal + } - StyledText { - Layout.topMargin: Appearance.spacing.large - Layout.alignment: Qt.AlignHCenter - text: qsTr("Wallpaper") - font.pointSize: Appearance.font.size.extraLarge - font.weight: 600 - } + Item { + Layout.fillWidth: true + } - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Select a wallpaper") - font.pointSize: Appearance.font.size.normal - color: Colours.palette.m3onSurfaceVariant - } + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: transparencyLayersInput.implicitHeight + Appearance.padding.small * 2 + color: transparencyLayersInputHover.containsMouse || transparencyLayersInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: transparencyLayersInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) - Item { - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.large - Layout.preferredHeight: wallpaperLoader.item ? wallpaperLoader.item.layoutPreferredHeight : 0 - - Loader { - id: wallpaperLoader - anchors.fill: parent - asynchronous: true - active: { - // Lazy load: only activate when: - // 1. Right pane is loaded AND - // 2. Appearance pane is active (index 3) or adjacent (for smooth transitions) - // This prevents loading all wallpapers when control center opens but appearance pane isn't visible - const isActive = root.session.activeIndex === 3; - const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1; - const shouldActivate = rightAppearanceLoader.item !== null && (isActive || isAdjacent); - return shouldActivate; - } - - onStatusChanged: { - if (status === Loader.Error) { - console.error("[AppearancePane] Wallpaper loader error!"); - } - } - - // Stop lazy loading when loader becomes inactive - onActiveChanged: { - if (!active && wallpaperLoader.item) { - const container = wallpaperLoader.item; - // Access timer through wallpaperGrid - if (container && container.wallpaperGrid) { - if (container.wallpaperGrid.scrollCheckTimer) { - container.wallpaperGrid.scrollCheckTimer.stop(); - } - container.wallpaperGrid._expansionInProgress = false; - } - } - } + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } - sourceComponent: Item { - id: wallpaperGridContainer - property alias layoutPreferredHeight: wallpaperGrid.layoutPreferredHeight - - // Find and store reference to parent Flickable for scroll monitoring - property var parentFlickable: { - let item = parent; - while (item) { - if (item.flickableDirection !== undefined) { - return item; - } - item = item.parent; - } - return null; - } - - // Cleanup when component is destroyed - Component.onDestruction: { - if (wallpaperGrid) { - if (wallpaperGrid.scrollCheckTimer) { - wallpaperGrid.scrollCheckTimer.stop(); - } - wallpaperGrid._expansionInProgress = false; - } - } - - // Lazy loading model: loads one image at a time, only when touching bottom - // This prevents GridView from creating all delegates at once - QtObject { - id: lazyModel - - property var sourceList: null - property int loadedCount: 0 // Total items available to load - property int visibleCount: 0 // Items actually exposed to GridView (only visible + buffer) - property int totalCount: 0 - - function initialize(list) { - sourceList = list; - totalCount = list ? list.length : 0; - // Start with enough items to fill the initial viewport (~3 rows) - const initialRows = 3; - const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 3; - const initialCount = Math.min(initialRows * cols, totalCount); - loadedCount = initialCount; - visibleCount = initialCount; - } - - function loadOneRow() { - if (loadedCount < totalCount) { - const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 1; - const itemsToLoad = Math.min(cols, totalCount - loadedCount); - loadedCount += itemsToLoad; - } - } - - function updateVisibleCount(neededCount) { - // Always round up to complete rows to avoid incomplete rows in the grid - const cols = wallpaperGrid.columnsCount > 0 ? wallpaperGrid.columnsCount : 1; - const maxVisible = Math.min(neededCount, loadedCount); - const rows = Math.ceil(maxVisible / cols); - const newVisibleCount = Math.min(rows * cols, loadedCount); - - if (newVisibleCount > visibleCount) { - visibleCount = newVisibleCount; - } - } - } - - GridView { - id: wallpaperGrid - anchors.fill: parent - - property int _delegateCount: 0 - - readonly property int minCellWidth: 200 + Appearance.spacing.normal - readonly property int columnsCount: Math.max(1, Math.floor(parent.width / minCellWidth)) - - // Height based on visible items only - prevents GridView from creating all delegates - readonly property int layoutPreferredHeight: { - if (!lazyModel || lazyModel.visibleCount === 0 || columnsCount === 0) { - return 0; - } - const calculated = Math.ceil(lazyModel.visibleCount / columnsCount) * cellHeight; - return calculated; - } - - height: layoutPreferredHeight - cellWidth: width / columnsCount - cellHeight: 140 + Appearance.spacing.normal - - leftMargin: 0 - rightMargin: 0 - topMargin: 0 - bottomMargin: 0 + MouseArea { + id: transparencyLayersInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } - // Use ListModel for incremental updates to prevent flashing when new items are added - ListModel { - id: wallpaperListModel - } - - model: wallpaperListModel - - Connections { - target: lazyModel - function onVisibleCountChanged(): void { - if (!lazyModel || !lazyModel.sourceList) return; - - const newCount = lazyModel.visibleCount; - const currentCount = wallpaperListModel.count; + StyledTextField { + id: transparencyLayersInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 100 } - // Only append new items - never remove or replace existing ones - if (newCount > currentCount) { - const flickable = wallpaperGridContainer.parentFlickable; - const oldScrollY = flickable ? flickable.contentY : 0; - - for (let i = currentCount; i < newCount; i++) { - wallpaperListModel.append({modelData: lazyModel.sourceList[i]}); - } - - // Preserve scroll position after model update - if (flickable) { - Qt.callLater(function() { - if (Math.abs(flickable.contentY - oldScrollY) < 1) { - flickable.contentY = oldScrollY; - } - }); - } - } - } - } - - Component.onCompleted: { - Qt.callLater(function() { - const isActive = root.session.activeIndex === 3; - if (width > 0 && parent && parent.visible && isActive && Wallpapers.list) { - lazyModel.initialize(Wallpapers.list); - wallpaperListModel.clear(); - for (let i = 0; i < lazyModel.visibleCount; i++) { - wallpaperListModel.append({modelData: lazyModel.sourceList[i]}); - } + Component.onCompleted: { + text = Math.round(rootPane.transparencyLayers * 100).toString(); } - }); - } - - Connections { - target: root.session - function onActiveIndexChanged(): void { - const isActive = root.session.activeIndex === 3; - // Stop lazy loading when switching away from appearance pane - if (!isActive) { - if (scrollCheckTimer) { - scrollCheckTimer.stop(); - } - if (wallpaperGrid) { - wallpaperGrid._expansionInProgress = false; + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + rootPane.transparencyLayers = val / 100; + rootPane.saveConfig(); + } } - return; } - - // Initialize if needed when switching to appearance pane - if (isActive && width > 0 && !lazyModel.sourceList && parent && parent.visible && Wallpapers.list) { - lazyModel.initialize(Wallpapers.list); - wallpaperListModel.clear(); - for (let i = 0; i < lazyModel.visibleCount; i++) { - wallpaperListModel.append({modelData: lazyModel.sourceList[i]}); + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(rootPane.transparencyLayers * 100).toString(); } } } } - - onWidthChanged: { - const isActive = root.session.activeIndex === 3; - if (width > 0 && !lazyModel.sourceList && parent && parent.visible && isActive && Wallpapers.list) { - lazyModel.initialize(Wallpapers.list); - wallpaperListModel.clear(); - for (let i = 0; i < lazyModel.visibleCount; i++) { - wallpaperListModel.append({modelData: lazyModel.sourceList[i]}); - } + + StyledText { + text: "%" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: layersSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0 + to: 100 + value: rootPane.transparencyLayers * 100 + onMoved: { + rootPane.transparencyLayers = layersSlider.value / 100; + if (!transparencyLayersInput.activeFocus) { + transparencyLayersInput.text = Math.round(layersSlider.value).toString(); } + rootPane.saveConfig(); } - - // Force true lazy loading: only create delegates for visible items - displayMarginBeginning: 0 - displayMarginEnd: 0 - cacheBuffer: 0 - - // Debounce expansion to avoid too frequent checks - property bool _expansionInProgress: false - - Connections { - target: wallpaperGridContainer.parentFlickable - function onContentYChanged(): void { - // Don't process scroll events if appearance pane is not active - const isActive = root.session.activeIndex === 3; - if (!isActive) return; + } + } + } + } + + CollapsibleSection { + id: borderSection + title: qsTr("Border") + showBackground: true + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Border rounding") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: borderRoundingInput.implicitHeight + Appearance.padding.small * 2 + color: borderRoundingInputHover.containsMouse || borderRoundingInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: borderRoundingInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: borderRoundingInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: borderRoundingInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0.1; top: 100 } - if (!lazyModel || !lazyModel.sourceList || lazyModel.loadedCount >= lazyModel.totalCount || wallpaperGrid._expansionInProgress) { - return; + Component.onCompleted: { + text = (rootPane.borderRounding).toFixed(1); } - const flickable = wallpaperGridContainer.parentFlickable; - if (!flickable) return; - - const gridY = wallpaperGridContainer.y; - const scrollY = flickable.contentY; - const viewportHeight = flickable.height; - - const topY = scrollY - gridY; - const bottomY = scrollY + viewportHeight - gridY; - - if (bottomY < 0) return; - - const topRow = Math.max(0, Math.floor(topY / wallpaperGrid.cellHeight)); - const bottomRow = Math.floor(bottomY / wallpaperGrid.cellHeight); - - // Update visible count with 1 row buffer ahead - const bufferRows = 1; - const neededBottomRow = bottomRow + bufferRows; - const neededCount = Math.min((neededBottomRow + 1) * wallpaperGrid.columnsCount, lazyModel.loadedCount); - lazyModel.updateVisibleCount(neededCount); - - // Load more when we're within 1 row of running out of loaded items - const loadedRows = Math.ceil(lazyModel.loadedCount / wallpaperGrid.columnsCount); - const rowsRemaining = loadedRows - (bottomRow + 1); - - if (rowsRemaining <= 1 && lazyModel.loadedCount < lazyModel.totalCount) { - if (!wallpaperGrid._expansionInProgress) { - wallpaperGrid._expansionInProgress = true; - lazyModel.loadOneRow(); - Qt.callLater(function() { - wallpaperGrid._expansionInProgress = false; - }); + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 100) { + rootPane.borderRounding = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 100) { + text = (rootPane.borderRounding).toFixed(1); } } } } - - // Fallback timer to check scroll position periodically - Timer { - id: scrollCheckTimer - interval: 100 - running: { - const isActive = root.session.activeIndex === 3; - return isActive && lazyModel && lazyModel.sourceList && lazyModel.loadedCount < lazyModel.totalCount; + } + + StyledSlider { + id: borderRoundingSlider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: 0.1 + to: 100 + value: rootPane.borderRounding + onMoved: { + rootPane.borderRounding = borderRoundingSlider.value; + if (!borderRoundingInput.activeFocus) { + borderRoundingInput.text = (borderRoundingSlider.value).toFixed(1); + } + rootPane.saveConfig(); + } + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Border thickness") + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: borderThicknessInput.implicitHeight + Appearance.padding.small * 2 + color: borderThicknessInputHover.containsMouse || borderThicknessInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: borderThicknessInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: borderThicknessInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton } - repeat: true - onTriggered: { - // Double-check that appearance pane is still active - const isActive = root.session.activeIndex === 3; - if (!isActive) { - stop(); - return; - } - - const flickable = wallpaperGridContainer.parentFlickable; - if (!flickable || !lazyModel || !lazyModel.sourceList) return; - - const gridY = wallpaperGridContainer.y; - const scrollY = flickable.contentY; - const viewportHeight = flickable.height; - - const topY = scrollY - gridY; - const bottomY = scrollY + viewportHeight - gridY; - if (bottomY < 0) return; - - const topRow = Math.max(0, Math.floor(topY / wallpaperGrid.cellHeight)); - const bottomRow = Math.floor(bottomY / wallpaperGrid.cellHeight); - - const bufferRows = 1; - const neededBottomRow = bottomRow + bufferRows; - const neededCount = Math.min((neededBottomRow + 1) * wallpaperGrid.columnsCount, lazyModel.loadedCount); - lazyModel.updateVisibleCount(neededCount); + + StyledTextField { + id: borderThicknessInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0.1; top: 100 } - // Load more when we're within 1 row of running out of loaded items - const loadedRows = Math.ceil(lazyModel.loadedCount / wallpaperGrid.columnsCount); - const rowsRemaining = loadedRows - (bottomRow + 1); + Component.onCompleted: { + text = (rootPane.borderThickness).toFixed(1); + } - if (rowsRemaining <= 1 && lazyModel.loadedCount < lazyModel.totalCount) { - if (!wallpaperGrid._expansionInProgress) { - wallpaperGrid._expansionInProgress = true; - lazyModel.loadOneRow(); - Qt.callLater(function() { - wallpaperGrid._expansionInProgress = false; - }); + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0.1 && val <= 100) { + rootPane.borderThickness = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0.1 || val > 100) { + text = (rootPane.borderThickness).toFixed(1); } } } } - - - // Parent Flickable handles scrolling - interactive: false + } + StyledSlider { + id: borderThicknessSlider - delegate: Item { - required property var modelData + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 - width: wallpaperGrid.cellWidth - height: wallpaperGrid.cellHeight + from: 0.1 + to: 100 + value: rootPane.borderThickness + onMoved: { + rootPane.borderThickness = borderThicknessSlider.value; + if (!borderThicknessInput.activeFocus) { + borderThicknessInput.text = (borderThicknessSlider.value).toFixed(1); + } + rootPane.saveConfig(); + } + } + } + } + } - readonly property bool isCurrent: modelData.path === Wallpapers.actualCurrent - readonly property real itemMargin: Appearance.spacing.normal / 2 - readonly property real itemRadius: Appearance.rounding.normal - - Component.onCompleted: { - wallpaperGrid._delegateCount++; + CollapsibleSection { + id: backgroundSection + title: qsTr("Background") + showBackground: true + + SwitchRow { + label: qsTr("Desktop clock") + checked: rootPane.desktopClockEnabled + onToggled: checked => { + rootPane.desktopClockEnabled = checked; + rootPane.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Background enabled") + checked: rootPane.backgroundEnabled + onToggled: checked => { + rootPane.backgroundEnabled = checked; + rootPane.saveConfig(); } + } - StateLayer { - anchors.fill: parent - anchors.leftMargin: itemMargin - anchors.rightMargin: itemMargin - anchors.topMargin: itemMargin - anchors.bottomMargin: itemMargin - radius: itemRadius + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Visualiser") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } - function onClicked(): void { - Wallpapers.setWallpaper(modelData.path); - } + SwitchRow { + label: qsTr("Visualiser enabled") + checked: rootPane.visualiserEnabled + onToggled: checked => { + rootPane.visualiserEnabled = checked; + rootPane.saveConfig(); } + } - StyledClippingRect { - id: image + SwitchRow { + label: qsTr("Visualiser auto hide") + checked: rootPane.visualiserAutoHide + onToggled: checked => { + rootPane.visualiserAutoHide = checked; + rootPane.saveConfig(); + } + } - anchors.fill: parent - anchors.leftMargin: itemMargin - anchors.rightMargin: itemMargin - anchors.topMargin: itemMargin - anchors.bottomMargin: itemMargin - color: Colours.tPalette.m3surfaceContainer - radius: itemRadius - antialiasing: true - layer.enabled: true - layer.smooth: true + SectionContainer { + contentSpacing: Appearance.spacing.normal - CachingImage { - id: cachingImage + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - path: modelData.path - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - cache: true - visible: opacity > 0 - antialiasing: true - smooth: true + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - opacity: status === Image.Ready ? 1 : 0 + StyledText { + text: qsTr("Visualiser rounding") + font.pointSize: Appearance.font.size.normal + } - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutQuad - } + Item { + Layout.fillWidth: true } - } - // Fallback if CachingImage fails to load - Image { - id: fallbackImage + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: visualiserRoundingInput.implicitHeight + Appearance.padding.small * 2 + color: visualiserRoundingInputHover.containsMouse || visualiserRoundingInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: visualiserRoundingInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) - anchors.fill: parent - source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : "" - asynchronous: true - fillMode: Image.PreserveAspectCrop - cache: true - visible: opacity > 0 - antialiasing: true - smooth: true + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } - opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 + MouseArea { + id: visualiserRoundingInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutQuad + StyledTextField { + id: visualiserRoundingInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 10 } + + Component.onCompleted: { + text = Math.round(rootPane.visualiserRounding).toString(); + } + + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 10) { + rootPane.visualiserRounding = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 10) { + text = Math.round(rootPane.visualiserRounding).toString(); + } + } } } } - Timer { - id: fallbackTimer - - property bool triggered: false - interval: 800 - running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null - onTriggered: triggered = true - } - - // Gradient overlay for filename - Rectangle { - id: filenameOverlay + StyledSlider { + id: visualiserRoundingSlider - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 - implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5 - radius: 0 - - gradient: Gradient { - GradientStop { - position: 0.0 - color: Qt.rgba(Colours.palette.m3surfaceContainer.r, - Colours.palette.m3surfaceContainer.g, - Colours.palette.m3surfaceContainer.b, 0) - } - GradientStop { - position: 0.3 - color: Qt.rgba(Colours.palette.m3surfaceContainer.r, - Colours.palette.m3surfaceContainer.g, - Colours.palette.m3surfaceContainer.b, 0.7) - } - GradientStop { - position: 0.6 - color: Qt.rgba(Colours.palette.m3surfaceContainer.r, - Colours.palette.m3surfaceContainer.g, - Colours.palette.m3surfaceContainer.b, 0.9) - } - GradientStop { - position: 1.0 - color: Qt.rgba(Colours.palette.m3surfaceContainer.r, - Colours.palette.m3surfaceContainer.g, - Colours.palette.m3surfaceContainer.b, 0.95) + from: 0 + to: 10 + stepSize: 1 + value: rootPane.visualiserRounding + onMoved: { + rootPane.visualiserRounding = Math.round(visualiserRoundingSlider.value); + if (!visualiserRoundingInput.activeFocus) { + visualiserRoundingInput.text = Math.round(visualiserRoundingSlider.value).toString(); } - } + rootPane.saveConfig(); + } + } + } + } - opacity: 0 + SectionContainer { + contentSpacing: Appearance.spacing.normal - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic - } - } + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - Component.onCompleted: { - opacity = 1; - } - } - } + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - Rectangle { - anchors.fill: parent - anchors.leftMargin: itemMargin - anchors.rightMargin: itemMargin - anchors.topMargin: itemMargin - anchors.bottomMargin: itemMargin - color: "transparent" - radius: itemRadius + border.width - border.width: isCurrent ? 2 : 0 - border.color: Colours.palette.m3primary - antialiasing: true - smooth: true + StyledText { + text: qsTr("Visualiser spacing") + font.pointSize: Appearance.font.size.normal + } - Behavior on border.width { - NumberAnimation { - duration: 150 - easing.type: Easing.OutQuad + Item { + Layout.fillWidth: true } - } - MaterialIcon { - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Appearance.padding.small + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: visualiserSpacingInput.implicitHeight + Appearance.padding.small * 2 + color: visualiserSpacingInputHover.containsMouse || visualiserSpacingInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: visualiserSpacingInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) - visible: isCurrent - text: "check_circle" - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large - } - } + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } - StyledText { - id: filenameText - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 - anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 - anchors.bottomMargin: Appearance.padding.normal + MouseArea { + id: visualiserSpacingInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } - readonly property string fileName: { - const path = modelData.relativePath || ""; - const parts = path.split("/"); - return parts.length > 0 ? parts[parts.length - 1] : path; + StyledTextField { + id: visualiserSpacingInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: DoubleValidator { bottom: 0; top: 2 } + + Component.onCompleted: { + text = (rootPane.visualiserSpacing).toFixed(1); + } + + onTextChanged: { + if (activeFocus) { + const val = parseFloat(text); + if (!isNaN(val) && val >= 0 && val <= 2) { + rootPane.visualiserSpacing = val; + rootPane.saveConfig(); + } + } + } + onEditingFinished: { + const val = parseFloat(text); + if (isNaN(val) || val < 0 || val > 2) { + text = (rootPane.visualiserSpacing).toFixed(1); + } + } + } + } } - text: fileName - font.pointSize: Appearance.font.size.smaller - font.weight: 500 - color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface - elide: Text.ElideMiddle - maximumLineCount: 1 - horizontalAlignment: Text.AlignHCenter + StyledSlider { + id: visualiserSpacingSlider - opacity: 0 + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic + from: 0 + to: 2 + value: rootPane.visualiserSpacing + onMoved: { + rootPane.visualiserSpacing = visualiserSpacingSlider.value; + if (!visualiserSpacingInput.activeFocus) { + visualiserSpacingInput.text = (visualiserSpacingSlider.value).toFixed(1); + } + rootPane.saveConfig(); } } - - Component.onCompleted: { - opacity = 1; - } - } - } - } } } } } } - } + } + + rightContent: appearanceRightContentComponent } } diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index c2d60d8..dc3ba56 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects @@ -11,52 +12,17 @@ import Quickshell.Widgets import QtQuick import QtQuick.Layouts -RowLayout { +Item { id: root required property Session session anchors.fill: parent - spacing: 0 + SplitPaneLayout { + anchors.fill: parent - Item { - id: leftAudioItem - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - ClippingRectangle { - id: leftAudioClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: leftAudioBorder.innerRadius - color: "transparent" - - Loader { - id: leftAudioLoader - - anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 - - asynchronous: true - sourceComponent: audioLeftContentComponent - } - } - - InnerBorder { - id: leftAudioBorder - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } - - Component { - id: audioLeftContentComponent + leftContent: Component { StyledFlickable { id: leftAudioFlickable @@ -246,349 +212,321 @@ RowLayout { } } } - } } - } - - Item { - id: rightAudioItem - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - id: rightAudioClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: rightAudioBorder.innerRadius - color: "transparent" - - Loader { - id: rightAudioLoader + } + rightContent: Component { + Item { anchors.fill: parent anchors.topMargin: Appearance.padding.large * 2 anchors.bottomMargin: Appearance.padding.large * 2 anchors.leftMargin: 0 anchors.rightMargin: 0 - asynchronous: true - sourceComponent: audioRightContentComponent - } - } - - InnerBorder { - id: rightAudioBorder - leftThickness: Appearance.padding.normal / 2 - } - - Component { - id: audioRightContentComponent + StyledFlickable { + id: rightAudioFlickable + anchors.fill: parent + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height - StyledFlickable { - id: rightAudioFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: rightAudioFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: rightAudioFlickable + } - ColumnLayout { - id: contentLayout + ColumnLayout { + id: contentLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Appearance.padding.large * 2 - anchors.rightMargin: Appearance.padding.large * 2 - spacing: Appearance.spacing.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Appearance.padding.large * 2 + anchors.rightMargin: Appearance.padding.large * 2 + spacing: Appearance.spacing.normal - ConnectionHeader { - icon: "volume_up" - title: qsTr("Audio Settings") - } + ConnectionHeader { + icon: "volume_up" + title: qsTr("Audio Settings") + } - SectionHeader { - title: qsTr("Output volume") - description: qsTr("Control the volume of your output device") - } + SectionHeader { + title: qsTr("Output volume") + description: qsTr("Control the volume of your output device") + } - SectionContainer { - contentSpacing: Appearance.spacing.normal + SectionContainer { + contentSpacing: Appearance.spacing.normal - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - StyledText { - text: qsTr("Volume") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } + StyledText { + text: qsTr("Volume") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: outputVolumeInput.implicitHeight + Appearance.padding.small * 2 - color: outputVolumeInputHover.containsMouse || outputVolumeInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: outputVolumeInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - enabled: !Audio.muted - opacity: enabled ? 1 : 0.5 - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: outputVolumeInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: outputVolumeInput.implicitHeight + Appearance.padding.small * 2 + color: outputVolumeInputHover.containsMouse || outputVolumeInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: outputVolumeInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + enabled: !Audio.muted + opacity: enabled ? 1 : 0.5 + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: outputVolumeInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } - StyledTextField { - id: outputVolumeInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } - enabled: !Audio.muted - - Component.onCompleted: { - text = Math.round(Audio.volume * 100).toString(); - } - - Connections { - target: Audio - function onVolumeChanged() { - if (!outputVolumeInput.activeFocus) { - outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); + StyledTextField { + id: outputVolumeInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 100 } + enabled: !Audio.muted + + Component.onCompleted: { + text = Math.round(Audio.volume * 100).toString(); } - } - } - - onTextChanged: { - if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - Audio.setVolume(val / 100); + + Connections { + target: Audio + function onVolumeChanged() { + if (!outputVolumeInput.activeFocus) { + outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); + } + } + } + + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + Audio.setVolume(val / 100); + } + } + } + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(Audio.volume * 100).toString(); + } } } } - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(Audio.volume * 100).toString(); - } + + StyledText { + text: "%" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + opacity: Audio.muted ? 0.5 : 1 } - } - } - StyledText { - text: "%" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - opacity: Audio.muted ? 0.5 : 1 - } + StyledRect { + implicitWidth: implicitHeight + implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2 - StyledRect { - implicitWidth: implicitHeight - implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2 + radius: Appearance.rounding.normal + color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - radius: Appearance.rounding.normal - color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + StateLayer { + function onClicked(): void { + if (Audio.sink?.audio) { + Audio.sink.audio.muted = !Audio.sink.audio.muted; + } + } + } - StateLayer { - function onClicked(): void { - if (Audio.sink?.audio) { - Audio.sink.audio.muted = !Audio.sink.audio.muted; + MaterialIcon { + id: muteIcon + + anchors.centerIn: parent + text: Audio.muted ? "volume_off" : "volume_up" + color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer } } } - MaterialIcon { - id: muteIcon + StyledSlider { + id: outputVolumeSlider + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 - anchors.centerIn: parent - text: Audio.muted ? "volume_off" : "volume_up" - color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + value: Audio.volume + enabled: !Audio.muted + opacity: enabled ? 1 : 0.5 + onMoved: { + Audio.setVolume(value); + if (!outputVolumeInput.activeFocus) { + outputVolumeInput.text = Math.round(value * 100).toString(); + } + } } } } - StyledSlider { - id: outputVolumeSlider - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - value: Audio.volume - enabled: !Audio.muted - opacity: enabled ? 1 : 0.5 - onMoved: { - Audio.setVolume(value); - if (!outputVolumeInput.activeFocus) { - outputVolumeInput.text = Math.round(value * 100).toString(); - } - } + SectionHeader { + title: qsTr("Input volume") + description: qsTr("Control the volume of your input device") } - } - } - SectionHeader { - title: qsTr("Input volume") - description: qsTr("Control the volume of your input device") - } - - SectionContainer { - contentSpacing: Appearance.spacing.normal + SectionContainer { + contentSpacing: Appearance.spacing.normal - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - StyledText { - text: qsTr("Volume") - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } + StyledText { + text: qsTr("Volume") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: inputVolumeInput.implicitHeight + Appearance.padding.small * 2 - color: inputVolumeInputHover.containsMouse || inputVolumeInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: inputVolumeInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - enabled: !Audio.sourceMuted - opacity: enabled ? 1 : 0.5 - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: inputVolumeInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: inputVolumeInput.implicitHeight + Appearance.padding.small * 2 + color: inputVolumeInputHover.containsMouse || inputVolumeInput.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: inputVolumeInput.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + enabled: !Audio.sourceMuted + opacity: enabled ? 1 : 0.5 + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: inputVolumeInputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } - StyledTextField { - id: inputVolumeInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } - enabled: !Audio.sourceMuted - - Component.onCompleted: { - text = Math.round(Audio.sourceVolume * 100).toString(); - } - - Connections { - target: Audio - function onSourceVolumeChanged() { - if (!inputVolumeInput.activeFocus) { - inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); + StyledTextField { + id: inputVolumeInput + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: IntValidator { bottom: 0; top: 100 } + enabled: !Audio.sourceMuted + + Component.onCompleted: { + text = Math.round(Audio.sourceVolume * 100).toString(); } - } - } - - onTextChanged: { - if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - Audio.setSourceVolume(val / 100); + + Connections { + target: Audio + function onSourceVolumeChanged() { + if (!inputVolumeInput.activeFocus) { + inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); + } + } + } + + onTextChanged: { + if (activeFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + Audio.setSourceVolume(val / 100); + } + } + } + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(Audio.sourceVolume * 100).toString(); + } } } } - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(Audio.sourceVolume * 100).toString(); - } + + StyledText { + text: "%" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + opacity: Audio.sourceMuted ? 0.5 : 1 } - } - } - StyledText { - text: "%" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - opacity: Audio.sourceMuted ? 0.5 : 1 - } + StyledRect { + implicitWidth: implicitHeight + implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2 - StyledRect { - implicitWidth: implicitHeight - implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2 + radius: Appearance.rounding.normal + color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - radius: Appearance.rounding.normal - color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + StateLayer { + function onClicked(): void { + if (Audio.source?.audio) { + Audio.source.audio.muted = !Audio.source.audio.muted; + } + } + } - StateLayer { - function onClicked(): void { - if (Audio.source?.audio) { - Audio.source.audio.muted = !Audio.source.audio.muted; + MaterialIcon { + id: muteInputIcon + + anchors.centerIn: parent + text: "mic_off" + color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer } } } - MaterialIcon { - id: muteInputIcon + StyledSlider { + id: inputVolumeSlider + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 - anchors.centerIn: parent - text: "mic_off" - color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - } - } - } - - StyledSlider { - id: inputVolumeSlider - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - value: Audio.sourceVolume - enabled: !Audio.sourceMuted - opacity: enabled ? 1 : 0.5 - onMoved: { - Audio.setSourceVolume(value); - if (!inputVolumeInput.activeFocus) { - inputVolumeInput.text = Math.round(value * 100).toString(); + value: Audio.sourceVolume + enabled: !Audio.sourceMuted + opacity: enabled ? 1 : 0.5 + onMoved: { + Audio.setSourceVolume(value); + if (!inputVolumeInput.activeFocus) { + inputVolumeInput.text = Math.round(value * 100).toString(); + } + } } } } } } } - } } } } \ No newline at end of file diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 8ad4b1f..cacb611 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components.controls import qs.components.effects import qs.components.containers @@ -10,95 +11,50 @@ import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts -RowLayout { +Item { id: root required property Session session anchors.fill: parent - spacing: 0 - - Item { - id: leftBtItem - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - ClippingRectangle { - id: leftBtClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: leftBtBorder.innerRadius - color: "transparent" - - Loader { - id: leftBtLoader - - anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 - - asynchronous: true - sourceComponent: btDeviceListComponent - } - } - - InnerBorder { - id: leftBtBorder - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } - - Component { - id: btDeviceListComponent + SplitPaneLayout { + anchors.fill: parent + leftContent: Component { DeviceList { anchors.fill: parent session: root.session } } - } - Item { - id: rightBtItem - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - id: btClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: rightBorder.innerRadius - color: "transparent" - - Loader { - id: loader + rightContent: Component { + Item { + id: rightBtPane property BluetoothDevice pane: root.session.bt.active - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 + Loader { + id: rightLoader - asynchronous: true - sourceComponent: pane ? details : settings + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + asynchronous: true + sourceComponent: rightBtPane.pane ? details : settings + } Behavior on pane { SequentialAnimation { ParallelAnimation { Anim { + target: rightLoader property: "opacity" to: 0 easing.bezierCurve: Appearance.anim.curves.standardAccel } Anim { + target: rightLoader property: "scale" to: 0.8 easing.bezierCurve: Appearance.anim.curves.standardAccel @@ -107,11 +63,13 @@ RowLayout { PropertyAction {} ParallelAnimation { Anim { + target: rightLoader property: "opacity" to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { + target: rightLoader property: "scale" to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel @@ -119,49 +77,49 @@ RowLayout { } } } - } - } - InnerBorder { - id: rightBorder - - leftThickness: Appearance.padding.normal / 2 + Connections { + target: root.session.bt + function onActiveChanged() { + rightBtPane.pane = root.session.bt.active; + } + } + } } + } - Component { - id: settings + Component { + id: settings - StyledFlickable { - id: settingsFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height + StyledFlickable { + id: settingsFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } - Settings { - id: settingsInner + Settings { + id: settingsInner - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session } } + } - Component { - id: details + Component { + id: details - Details { - session: root.session - } + Details { + session: root.session } } component Anim: NumberAnimation { - target: loader duration: Appearance.anim.durations.normal / 2 easing.type: Easing.BezierSpline } diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml new file mode 100644 index 0000000..7bd7db0 --- /dev/null +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -0,0 +1,120 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.effects +import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + spacing: 0 + + property Component leftContent: null + property Component rightContent: null + + // Left pane configuration + property real leftWidthRatio: 0.4 + property int leftMinimumWidth: 420 + property var leftLoaderProperties: ({}) + + // Right pane configuration + property var rightLoaderProperties: ({}) + + // Expose loaders for customization (access via splitLayout.leftLoader or splitLayout.rightLoader) + property alias leftLoader: leftLoader + property alias rightLoader: rightLoader + + // Left pane + Item { + id: leftPane + + Layout.preferredWidth: Math.floor(parent.width * root.leftWidthRatio) + Layout.minimumWidth: root.leftMinimumWidth + Layout.fillHeight: true + + ClippingRectangle { + id: leftClippingRect + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + + radius: leftBorder.innerRadius + color: "transparent" + + Loader { + id: leftLoader + + anchors.fill: parent + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 + + asynchronous: true + sourceComponent: root.leftContent + + // Apply any additional properties from leftLoaderProperties + Component.onCompleted: { + for (const key in root.leftLoaderProperties) { + leftLoader[key] = root.leftLoaderProperties[key]; + } + } + } + } + + InnerBorder { + id: leftBorder + + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + // Right pane + Item { + id: rightPane + + Layout.fillWidth: true + Layout.fillHeight: true + + ClippingRectangle { + id: rightClippingRect + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + + radius: rightBorder.innerRadius + color: "transparent" + + Loader { + id: rightLoader + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + asynchronous: true + sourceComponent: root.rightContent + + // Apply any additional properties from rightLoaderProperties + Component.onCompleted: { + for (const key in root.rightLoaderProperties) { + rightLoader[key] = root.rightLoaderProperties[key]; + } + } + } + } + + InnerBorder { + id: rightBorder + + leftThickness: Appearance.padding.normal / 2 + } + } +} + diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index f2247a7..30e2953 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import "../../launcher/services" import qs.components import qs.components.controls @@ -16,7 +17,7 @@ import QtQuick import QtQuick.Layouts import "../../../utils/scripts/fuzzysort.js" as Fuzzy -RowLayout { +Item { id: root required property Session session @@ -26,8 +27,6 @@ RowLayout { anchors.fill: parent - spacing: 0 - onSelectedAppChanged: { root.session.launcher.active = root.selectedApp; updateToggleState(); @@ -156,43 +155,10 @@ RowLayout { } } - Item { - id: leftLauncherItem - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - ClippingRectangle { - id: leftLauncherClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: leftLauncherBorder.innerRadius - color: "transparent" - - Loader { - id: leftLauncherLoader - - anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 - - asynchronous: true - sourceComponent: leftContentComponent - } - } - - InnerBorder { - id: leftLauncherBorder - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } + SplitPaneLayout { + anchors.fill: parent - Component { - id: leftContentComponent + leftContent: Component { ColumnLayout { id: leftLauncherLayout @@ -336,7 +302,8 @@ RowLayout { // Lazy load: activate when left pane is loaded // The ListView will load asynchronously, and search will work because filteredApps // is updated regardless of whether the ListView is loaded - return leftLauncherLoader.item !== null; + // Access loader through parent - this will be set when component loads + return true; } sourceComponent: StyledListView { @@ -412,25 +379,10 @@ RowLayout { } } } - } - - Item { - id: rightLauncherItem - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - id: rightLauncherClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - radius: rightLauncherBorder.innerRadius - color: "transparent" - - Loader { - id: rightLauncherLoader + rightContent: Component { + Item { + id: rightLauncherPane property var pane: root.session.launcher.active property string paneId: pane ? (pane.id || pane.entry?.id || "") : "" @@ -442,28 +394,34 @@ RowLayout { return pane ? appDetails : settings; } - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - - opacity: 1 - scale: 1 - transformOrigin: Item.Center - clip: false - - asynchronous: true - sourceComponent: rightLauncherLoader.targetComponent - active: true - Component.onCompleted: { displayedApp = pane; targetComponent = getComponentForPane(); nextComponent = targetComponent; } - onItemChanged: { - // Ensure displayedApp is set when item is created (for async loading) - if (item && pane && displayedApp !== pane) { - displayedApp = pane; + Loader { + id: rightLauncherLoader + + anchors.fill: parent + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + clip: false + + asynchronous: true + sourceComponent: rightLauncherPane.targetComponent + active: true + + // Expose displayedApp to loaded components + property var displayedApp: rightLauncherPane.displayedApp + + onItemChanged: { + // Ensure displayedApp is set when item is created (for async loading) + if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) { + rightLauncherPane.displayedApp = rightLauncherPane.pane; + } } } @@ -484,9 +442,9 @@ RowLayout { } } PropertyAction { - target: rightLauncherLoader + target: rightLauncherPane property: "displayedApp" - value: rightLauncherLoader.pane + value: rightLauncherPane.pane } PropertyAction { target: rightLauncherLoader @@ -494,9 +452,9 @@ RowLayout { value: false } PropertyAction { - target: rightLauncherLoader + target: rightLauncherPane property: "targetComponent" - value: rightLauncherLoader.nextComponent + value: rightLauncherPane.nextComponent } PropertyAction { target: rightLauncherLoader @@ -539,90 +497,90 @@ RowLayout { } } } + } - InnerBorder { - id: rightLauncherBorder - - leftThickness: Appearance.padding.normal / 2 - } - - Component { - id: settings + Component { + id: settings - StyledFlickable { - id: settingsFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height + StyledFlickable { + id: settingsFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } - Settings { - id: settingsInner + Settings { + id: settingsInner - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session } } + } - Component { - id: appDetails + Component { + id: appDetails - ColumnLayout { - anchors.fill: parent + ColumnLayout { + id: appDetailsLayout + anchors.fill: parent - spacing: Appearance.spacing.normal + // Get displayedApp from parent Loader (the Loader has displayedApp property we set) + readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null - Item { - Layout.alignment: Qt.AlignHCenter - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 - Layout.topMargin: Appearance.padding.large * 2 - implicitWidth: iconLoader.implicitWidth - implicitHeight: iconLoader.implicitHeight + spacing: Appearance.spacing.normal - Loader { - id: iconLoader - sourceComponent: rightLauncherLoader.displayedApp ? appIconComponent : defaultIconComponent - } + Item { + Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: Appearance.padding.large * 2 + Layout.rightMargin: Appearance.padding.large * 2 + Layout.topMargin: Appearance.padding.large * 2 + implicitWidth: iconLoader.implicitWidth + implicitHeight: iconLoader.implicitHeight - Component { - id: appIconComponent - IconImage { - implicitSize: Appearance.font.size.extraLarge * 3 * 2 - source: { - if (!rightLauncherLoader.displayedApp) return "image-missing"; - const entry = rightLauncherLoader.displayedApp.entry; - if (entry && entry.icon) { - return Quickshell.iconPath(entry.icon, "image-missing"); - } - return "image-missing"; + Loader { + id: iconLoader + sourceComponent: parent.parent.displayedApp ? appIconComponent : defaultIconComponent + } + + Component { + id: appIconComponent + IconImage { + implicitSize: Appearance.font.size.extraLarge * 3 * 2 + source: { + const app = iconLoader.parent.parent.displayedApp; + if (!app) return "image-missing"; + const entry = app.entry; + if (entry && entry.icon) { + return Quickshell.iconPath(entry.icon, "image-missing"); } + return "image-missing"; } } + } - Component { - id: defaultIconComponent - MaterialIcon { - text: "apps" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } + Component { + id: defaultIconComponent + MaterialIcon { + text: "apps" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true } } + } - StyledText { - Layout.alignment: Qt.AlignHCenter - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 - text: rightLauncherLoader.displayedApp ? (rightLauncherLoader.displayedApp.name || rightLauncherLoader.displayedApp.entry?.name || qsTr("Application Details")) : qsTr("Launcher Applications") - font.pointSize: Appearance.font.size.large - font.bold: true - } + StyledText { + Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: Appearance.padding.large * 2 + Layout.rightMargin: Appearance.padding.large * 2 + text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : qsTr("Launcher Applications") + font.pointSize: Appearance.font.size.large + font.bold: true + } Item { Layout.fillWidth: true @@ -648,38 +606,38 @@ RowLayout { anchors.top: parent.top spacing: Appearance.spacing.normal - SwitchRow { - Layout.topMargin: Appearance.spacing.normal - visible: rightLauncherLoader.displayedApp !== null - label: qsTr("Hide from launcher") - checked: root.hideFromLauncherChecked - enabled: rightLauncherLoader.displayedApp !== null - onToggled: checked => { - root.hideFromLauncherChecked = checked; - if (rightLauncherLoader.displayedApp) { - const appId = rightLauncherLoader.displayedApp.id || rightLauncherLoader.displayedApp.entry?.id; - const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; - if (checked) { - if (!hiddenApps.includes(appId)) { - hiddenApps.push(appId); - } - } else { - const index = hiddenApps.indexOf(appId); - if (index !== -1) { - hiddenApps.splice(index, 1); - } + SwitchRow { + Layout.topMargin: Appearance.spacing.normal + visible: appDetailsLayout.displayedApp !== null + label: qsTr("Hide from launcher") + checked: root.hideFromLauncherChecked + enabled: appDetailsLayout.displayedApp !== null + onToggled: checked => { + root.hideFromLauncherChecked = checked; + const app = appDetailsLayout.displayedApp; + if (app) { + const appId = app.id || app.entry?.id; + const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; + if (checked) { + if (!hiddenApps.includes(appId)) { + hiddenApps.push(appId); + } + } else { + const index = hiddenApps.indexOf(appId); + if (index !== -1) { + hiddenApps.splice(index, 1); } - Config.launcher.hiddenApps = hiddenApps; - Config.save(); } + Config.launcher.hiddenApps = hiddenApps; + Config.save(); } } + } } } } } - } } component Anim: NumberAnimation { diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index d0ea852..55c70d2 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import "." import qs.components import qs.components.controls @@ -21,49 +22,12 @@ Item { anchors.fill: parent - RowLayout { - id: contentLayout + SplitPaneLayout { + id: splitLayout anchors.fill: parent - spacing: 0 - Item { - id: leftNetworkItem - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - ClippingRectangle { - id: leftNetworkClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: leftNetworkBorder.innerRadius - color: "transparent" - - Loader { - id: leftNetworkLoader - - anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 - - asynchronous: true - sourceComponent: networkListComponent - } - } - - InnerBorder { - id: leftNetworkBorder - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } - - Component { - id: networkListComponent + leftContent: Component { StyledFlickable { id: leftFlickable @@ -473,38 +437,47 @@ Item { } } } - } } - Item { - id: rightNetworkItem - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - id: networkClippingRect - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: rightBorder.innerRadius - color: "transparent" - + rightContent: Component { + Item { + id: rightPaneItem + // Right pane - networking details/settings - Loader { - id: loader + property var ethernetPane: root.session.ethernet.active + property var wirelessPane: root.session.network.active + property var pane: ethernetPane || wirelessPane + property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "") + property Component targetComponent: settings + property Component nextComponent: settings + + function getComponentForPane() { + return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings; + } - property var ethernetPane: root.session.ethernet.active - property var wirelessPane: root.session.network.active - property var pane: ethernetPane || wirelessPane - property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "") - property Component targetComponent: settings - property Component nextComponent: settings + Component.onCompleted: { + targetComponent = getComponentForPane(); + nextComponent = targetComponent; + } - function getComponentForPane() { - return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings; + Connections { + target: root.session.ethernet + function onActiveChanged() { + nextComponent = getComponentForPane(); + paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + } + } + + Connections { + target: root.session.network + function onActiveChanged() { + nextComponent = getComponentForPane(); + paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); } + } + + Loader { + id: rightLoader anchors.fill: parent anchors.margins: Appearance.padding.large * 2 @@ -515,131 +488,129 @@ Item { clip: false asynchronous: true - sourceComponent: loader.targetComponent + sourceComponent: rightPaneItem.targetComponent - Component.onCompleted: { - targetComponent = getComponentForPane(); - nextComponent = targetComponent; + Connections { + target: rightPaneItem + function onPaneIdChanged() { + rightPaneItem.targetComponent = rightPaneItem.nextComponent; + } } + } - Behavior on paneId { - SequentialAnimation { - ParallelAnimation { - Anim { - target: loader - property: "opacity" - to: 0 - easing.bezierCurve: Appearance.anim.curves.standardAccel - } - Anim { - target: loader - property: "scale" - to: 0.8 - easing.bezierCurve: Appearance.anim.curves.standardAccel - } + Behavior on paneId { + SequentialAnimation { + ParallelAnimation { + Anim { + target: rightLoader + property: "opacity" + to: 0 + easing.bezierCurve: Appearance.anim.curves.standardAccel } - PropertyAction { - target: loader - property: "targetComponent" - value: loader.nextComponent + Anim { + target: rightLoader + property: "scale" + to: 0.8 + easing.bezierCurve: Appearance.anim.curves.standardAccel } - ParallelAnimation { - Anim { - target: loader - property: "opacity" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - Anim { - target: loader - property: "scale" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } + } + PropertyAction { + target: rightPaneItem + property: "targetComponent" + value: rightPaneItem.nextComponent + } + ParallelAnimation { + Anim { + target: rightLoader + property: "opacity" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + target: rightLoader + property: "scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel } } } + } - onPaneChanged: { - nextComponent = getComponentForPane(); - paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + Connections { + target: rightPaneItem + function onPaneIdChanged() { + rightPaneItem.targetComponent = rightPaneItem.nextComponent; } } } + } + } - InnerBorder { - id: rightBorder - - leftThickness: Appearance.padding.normal / 2 - } - - Component { - id: settings + Component { + id: settings - StyledFlickable { - id: settingsFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height + StyledFlickable { + id: settingsFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } - NetworkSettings { - id: settingsInner + NetworkSettings { + id: settingsInner - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } - } + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session } + } + } - Component { - id: ethernetDetails + Component { + id: ethernetDetails - StyledFlickable { - id: ethernetFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: ethernetDetailsInner.height + StyledFlickable { + id: ethernetFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: ethernetDetailsInner.height - StyledScrollBar.vertical: StyledScrollBar { - flickable: ethernetFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: ethernetFlickable + } - EthernetDetails { - id: ethernetDetailsInner + EthernetDetails { + id: ethernetDetailsInner - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } - } + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session } + } + } - Component { - id: wirelessDetails + Component { + id: wirelessDetails - StyledFlickable { - id: wirelessFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: wirelessDetailsInner.height + StyledFlickable { + id: wirelessFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: wirelessDetailsInner.height - StyledScrollBar.vertical: StyledScrollBar { - flickable: wirelessFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: wirelessFlickable + } - WirelessDetails { - id: wirelessDetailsInner + WirelessDetails { + id: wirelessDetailsInner - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - session: root.session - } - } + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session } } } @@ -651,7 +622,6 @@ Item { } component Anim: NumberAnimation { - target: loader duration: Appearance.anim.durations.normal / 2 easing.type: Easing.BezierSpline } -- cgit v1.2.3-freya From aff01f01a859f0d959c76d67f346b34a714cb971 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 13:10:09 -0500 Subject: refactor: PaneTransition now component --- modules/controlcenter/bluetooth/BtPane.qml | 60 +++++++--------- .../controlcenter/components/PaneTransition.qml | 84 ++++++++++++++++++++++ modules/controlcenter/launcher/LauncherPane.qml | 73 ++++++------------- modules/controlcenter/network/EthernetPane.qml | 45 ++---------- modules/controlcenter/network/NetworkingPane.qml | 70 ++++-------------- modules/controlcenter/network/WirelessPane.qml | 45 ++---------- 6 files changed, 156 insertions(+), 221 deletions(-) create mode 100644 modules/controlcenter/components/PaneTransition.qml diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index cacb611..9d0a4c1 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -33,6 +33,18 @@ Item { id: rightBtPane property BluetoothDevice pane: root.session.bt.active + property string paneId: pane ? (pane.address || "") : "" + property Component targetComponent: settings + property Component nextComponent: settings + + function getComponentForPane() { + return pane ? details : settings; + } + + Component.onCompleted: { + targetComponent = getComponentForPane(); + nextComponent = targetComponent; + } Loader { id: rightLoader @@ -41,40 +53,19 @@ Item { anchors.margins: Appearance.padding.large * 2 asynchronous: true - sourceComponent: rightBtPane.pane ? details : settings + sourceComponent: rightBtPane.targetComponent } - 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 + Behavior on paneId { + PaneTransition { + target: rightLoader + propertyActions: [ + PropertyAction { + target: rightBtPane + property: "targetComponent" + value: rightBtPane.nextComponent } - } - 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 - } - } + ] } } @@ -82,6 +73,8 @@ Item { target: root.session.bt function onActiveChanged() { rightBtPane.pane = root.session.bt.active; + rightBtPane.nextComponent = rightBtPane.getComponentForPane(); + rightBtPane.paneId = pane ? (pane.address || "") : ""; } } } @@ -118,9 +111,4 @@ Item { session: root.session } } - - component Anim: NumberAnimation { - duration: Appearance.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - } } diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml new file mode 100644 index 0000000..1da4afb --- /dev/null +++ b/modules/controlcenter/components/PaneTransition.qml @@ -0,0 +1,84 @@ +pragma ComponentBehavior: Bound + +import qs.config +import QtQuick + +// Reusable pane transition animation component +// Provides standard fade-out/scale-down → update → fade-in/scale-up animation +// Used when switching between detail/settings views in panes +SequentialAnimation { + id: root + + // The Loader element to animate + required property Item target + + // Optional list of PropertyActions to execute during the transition + // These typically update the component being displayed + property list propertyActions + + // Animation parameters (with sensible defaults) + property real scaleFrom: 1.0 + property real scaleTo: 0.8 + property real opacityFrom: 1.0 + property real opacityTo: 0.0 + + // Fade out and scale down + ParallelAnimation { + NumberAnimation { + target: root.target + property: "opacity" + from: root.opacityFrom + to: root.opacityTo + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + + NumberAnimation { + target: root.target + property: "scale" + from: root.scaleFrom + to: root.scaleTo + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + } + + // Execute property actions (component switching, state updates, etc.) + // This is where the component change happens while invisible + ScriptAction { + script: { + for (let i = 0; i < root.propertyActions.length; i++) { + const action = root.propertyActions[i]; + if (action.target && action.property !== undefined) { + action.target[action.property] = action.value; + } + } + } + } + + // Fade in and scale up + ParallelAnimation { + NumberAnimation { + target: root.target + property: "opacity" + from: root.opacityTo + to: root.opacityFrom + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + + NumberAnimation { + target: root.target + property: "scale" + from: root.scaleTo + to: root.scaleFrom + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } +} + diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 30e2953..a8c5c76 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -426,55 +426,30 @@ Item { } Behavior on paneId { - SequentialAnimation { - ParallelAnimation { - Anim { + PaneTransition { + target: rightLauncherLoader + propertyActions: [ + PropertyAction { + target: rightLauncherPane + property: "displayedApp" + value: rightLauncherPane.pane + }, + PropertyAction { target: rightLauncherLoader - property: "opacity" - to: 0 - easing.bezierCurve: Appearance.anim.curves.standardAccel - } - Anim { + property: "active" + value: false + }, + PropertyAction { + target: rightLauncherPane + property: "targetComponent" + value: rightLauncherPane.nextComponent + }, + PropertyAction { target: rightLauncherLoader - property: "scale" - to: 0.8 - easing.bezierCurve: Appearance.anim.curves.standardAccel + property: "active" + value: true } - } - PropertyAction { - target: rightLauncherPane - property: "displayedApp" - value: rightLauncherPane.pane - } - PropertyAction { - target: rightLauncherLoader - property: "active" - value: false - } - PropertyAction { - target: rightLauncherPane - property: "targetComponent" - value: rightLauncherPane.nextComponent - } - PropertyAction { - target: rightLauncherLoader - property: "active" - value: true - } - ParallelAnimation { - Anim { - target: rightLauncherLoader - property: "opacity" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - Anim { - target: rightLauncherLoader - property: "scale" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - } + ] } } @@ -639,10 +614,4 @@ Item { } } } - - component Anim: NumberAnimation { - target: rightLauncherLoader - duration: Appearance.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - } } diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml index 6a50cde..fc979c3 100644 --- a/modules/controlcenter/network/EthernetPane.qml +++ b/modules/controlcenter/network/EthernetPane.qml @@ -77,40 +77,15 @@ RowLayout { } Behavior on paneId { - SequentialAnimation { - ParallelAnimation { - Anim { + PaneTransition { + target: loader + propertyActions: [ + PropertyAction { target: loader - property: "opacity" - to: 0 - easing.bezierCurve: Appearance.anim.curves.standardAccel + property: "targetComponent" + value: loader.nextComponent } - Anim { - target: loader - property: "scale" - to: 0.8 - easing.bezierCurve: Appearance.anim.curves.standardAccel - } - } - PropertyAction { - target: loader - property: "targetComponent" - value: loader.nextComponent - } - 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 - } - } + ] } } @@ -153,10 +128,4 @@ RowLayout { } } } - - component Anim: NumberAnimation { - target: loader - duration: Appearance.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - } } \ No newline at end of file diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 55c70d2..127a42a 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -447,7 +447,7 @@ Item { 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 string paneId: ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings") property Component targetComponent: settings property Component nextComponent: settings @@ -463,16 +463,16 @@ Item { Connections { target: root.session.ethernet function onActiveChanged() { - nextComponent = getComponentForPane(); - paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); + rightPaneItem.paneId = rightPaneItem.ethernetPane ? ("eth:" + (rightPaneItem.ethernetPane.interface || "")) : (rightPaneItem.wirelessPane ? ("wifi:" + (rightPaneItem.wirelessPane.ssid || rightPaneItem.wirelessPane.bssid || "")) : "settings"); } } Connections { target: root.session.network function onActiveChanged() { - nextComponent = getComponentForPane(); - paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); + rightPaneItem.paneId = rightPaneItem.ethernetPane ? ("eth:" + (rightPaneItem.ethernetPane.interface || "")) : (rightPaneItem.wirelessPane ? ("wifi:" + (rightPaneItem.wirelessPane.ssid || rightPaneItem.wirelessPane.bssid || "")) : "settings"); } } @@ -489,57 +489,18 @@ Item { asynchronous: true sourceComponent: rightPaneItem.targetComponent - - Connections { - target: rightPaneItem - function onPaneIdChanged() { - rightPaneItem.targetComponent = rightPaneItem.nextComponent; - } - } } Behavior on paneId { - 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 - } - } - 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 + PaneTransition { + target: rightLoader + propertyActions: [ + PropertyAction { + target: rightPaneItem + property: "targetComponent" + value: rightPaneItem.nextComponent } - } - } - } - - Connections { - target: rightPaneItem - function onPaneIdChanged() { - rightPaneItem.targetComponent = rightPaneItem.nextComponent; + ] } } } @@ -621,11 +582,6 @@ Item { z: 1000 } - component Anim: NumberAnimation { - duration: Appearance.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - } - function checkSavedProfileForNetwork(ssid: string): void { if (ssid && ssid.length > 0) { Nmcli.loadSavedConnections(() => {}); diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index 22364a1..0c8f0ae 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -76,40 +76,15 @@ RowLayout { } Behavior on paneId { - SequentialAnimation { - ParallelAnimation { - Anim { + PaneTransition { + target: loader + propertyActions: [ + PropertyAction { target: loader - property: "opacity" - to: 0 - easing.bezierCurve: Appearance.anim.curves.standardAccel + property: "targetComponent" + value: loader.nextComponent } - Anim { - target: loader - property: "scale" - to: 0.8 - easing.bezierCurve: Appearance.anim.curves.standardAccel - } - } - PropertyAction { - target: loader - property: "targetComponent" - value: loader.nextComponent - } - 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 - } - } + ] } } @@ -158,10 +133,4 @@ RowLayout { session: root.session z: 1000 } - - component Anim: NumberAnimation { - target: loader - duration: Appearance.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - } } \ No newline at end of file -- cgit v1.2.3-freya From d3ecbc1b647ee7cf074a8f06384d68168657d9c6 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 13:21:18 -0500 Subject: controlcenter: correcting margins/padding after refactor --- modules/controlcenter/audio/AudioPane.qml | 40 +++++++++--------------- modules/controlcenter/bluetooth/BtPane.qml | 1 - modules/controlcenter/network/NetworkingPane.qml | 1 - 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index dc3ba56..02cda5b 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -216,31 +216,22 @@ Item { } rightContent: Component { - Item { - anchors.fill: parent - anchors.topMargin: Appearance.padding.large * 2 - anchors.bottomMargin: Appearance.padding.large * 2 - anchors.leftMargin: 0 - anchors.rightMargin: 0 - - StyledFlickable { - id: rightAudioFlickable - anchors.fill: parent - flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: rightAudioFlickable - } + StyledFlickable { + id: rightAudioFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height - ColumnLayout { - id: contentLayout + StyledScrollBar.vertical: StyledScrollBar { + flickable: rightAudioFlickable + } - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Appearance.padding.large * 2 - anchors.rightMargin: Appearance.padding.large * 2 - spacing: Appearance.spacing.normal + ColumnLayout { + id: contentLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal ConnectionHeader { icon: "volume_up" @@ -528,5 +519,4 @@ Item { } } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 9d0a4c1..c20f14b 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -50,7 +50,6 @@ Item { id: rightLoader anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 asynchronous: true sourceComponent: rightBtPane.targetComponent diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 127a42a..9a7a4e1 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -480,7 +480,6 @@ Item { id: rightLoader anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 opacity: 1 scale: 1 -- cgit v1.2.3-freya From 1c523240d84edc8a947d0063e3b9a707b6d818c0 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 13:48:06 -0500 Subject: refactor: added state module --- modules/controlcenter/Session.qml | 33 +++++--------------------- modules/controlcenter/state/BluetoothState.qml | 18 ++++++++++++++ modules/controlcenter/state/EthernetState.qml | 9 +++++++ modules/controlcenter/state/LauncherState.qml | 9 +++++++ modules/controlcenter/state/NetworkState.qml | 13 ++++++++++ 5 files changed, 55 insertions(+), 27 deletions(-) create mode 100644 modules/controlcenter/state/BluetoothState.qml create mode 100644 modules/controlcenter/state/EthernetState.qml create mode 100644 modules/controlcenter/state/LauncherState.qml create mode 100644 modules/controlcenter/state/NetworkState.qml diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index 6f560f5..5d7b094 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -1,5 +1,5 @@ -import Quickshell.Bluetooth import QtQuick +import "./state" QtObject { readonly property list panes: ["network", "bluetooth", "audio", "appearance", "taskbar", "launcher"] @@ -10,33 +10,12 @@ QtObject { property int activeIndex: 0 property bool navExpanded: false - readonly property Bt bt: Bt {} - readonly property Network network: Network {} - readonly property Ethernet ethernet: Ethernet {} - readonly property Launcher launcher: Launcher {} + // Pane-specific state objects + readonly property BluetoothState bt: BluetoothState {} + readonly property NetworkState network: NetworkState {} + readonly property EthernetState ethernet: EthernetState {} + readonly property LauncherState launcher: LauncherState {} onActiveChanged: activeIndex = panes.indexOf(active) onActiveIndexChanged: active = panes[activeIndex] - - component Bt: QtObject { - property BluetoothDevice active - property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter - property bool editingAdapterName - property bool fabMenuOpen - property bool editingDeviceName - } - - component Network: QtObject { - property var active - property bool showPasswordDialog: false - property var pendingNetwork - } - - component Ethernet: QtObject { - property var active - } - - component Launcher: QtObject { - property var active - } } diff --git a/modules/controlcenter/state/BluetoothState.qml b/modules/controlcenter/state/BluetoothState.qml new file mode 100644 index 0000000..db8c7e1 --- /dev/null +++ b/modules/controlcenter/state/BluetoothState.qml @@ -0,0 +1,18 @@ +import Quickshell.Bluetooth +import QtQuick + +QtObject { + id: root + + // Active selected device + property BluetoothDevice active: null + + // Current adapter being used + property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter + + // UI state flags + property bool editingAdapterName: false + property bool fabMenuOpen: false + property bool editingDeviceName: false +} + diff --git a/modules/controlcenter/state/EthernetState.qml b/modules/controlcenter/state/EthernetState.qml new file mode 100644 index 0000000..25b243a --- /dev/null +++ b/modules/controlcenter/state/EthernetState.qml @@ -0,0 +1,9 @@ +import QtQuick + +QtObject { + id: root + + // Active selected ethernet interface + property var active: null +} + diff --git a/modules/controlcenter/state/LauncherState.qml b/modules/controlcenter/state/LauncherState.qml new file mode 100644 index 0000000..cd9eeb6 --- /dev/null +++ b/modules/controlcenter/state/LauncherState.qml @@ -0,0 +1,9 @@ +import QtQuick + +QtObject { + id: root + + // Active selected application + property var active: null +} + diff --git a/modules/controlcenter/state/NetworkState.qml b/modules/controlcenter/state/NetworkState.qml new file mode 100644 index 0000000..651a35c --- /dev/null +++ b/modules/controlcenter/state/NetworkState.qml @@ -0,0 +1,13 @@ +import QtQuick + +QtObject { + id: root + + // Active selected wireless network + property var active: null + + // Password dialog state + property bool showPasswordDialog: false + property var pendingNetwork: null +} + -- cgit v1.2.3-freya From 50dd4e1c441db43bc4dee5d5007f28c52cef848b Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 14:07:53 -0500 Subject: refactor: created pane registry --- modules/controlcenter/NavRail.qml | 38 +++------- modules/controlcenter/PaneRegistry.qml | 133 +++++++++++++++++++++++++++++++++ modules/controlcenter/Panes.qml | 58 +++++--------- modules/controlcenter/Session.qml | 3 +- 4 files changed, 161 insertions(+), 71 deletions(-) create mode 100644 modules/controlcenter/PaneRegistry.qml diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 1de1a9e..ef338b2 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -117,35 +117,15 @@ Item { } } - NavItem { - Layout.topMargin: Appearance.spacing.large * 2 - icon: "router" - label: "network" - } - - NavItem { - icon: "settings_bluetooth" - label: "bluetooth" - } - - NavItem { - icon: "volume_up" - label: "audio" - } - - NavItem { - icon: "palette" - label: "appearance" - } - - NavItem { - icon: "task_alt" - label: "taskbar" - } - - NavItem { - icon: "apps" - label: "launcher" + Repeater { + model: PaneRegistry.count + + NavItem { + required property int index + Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0 + icon: PaneRegistry.getByIndex(index).icon + label: PaneRegistry.getByIndex(index).label + } } } diff --git a/modules/controlcenter/PaneRegistry.qml b/modules/controlcenter/PaneRegistry.qml new file mode 100644 index 0000000..df63c6e --- /dev/null +++ b/modules/controlcenter/PaneRegistry.qml @@ -0,0 +1,133 @@ +pragma Singleton + +import QtQuick + +/** + * PaneRegistry + * + * Centralized registry for Control Center panes. This singleton provides a single + * source of truth for pane metadata (id, label, icon, component), eliminating + * the need for manual index management and making adding/removing panes trivial. + * + * Usage: + * - Panes.qml: Dynamically creates panes from registry + * - Session.qml: Derives panes list from registry + * - NavRail.qml: Uses registry for navigation items + */ +QtObject { + id: root + + /** + * Pane metadata structure: + * - id: Unique identifier for the pane (string) + * - label: Display label for the pane (string) + * - icon: Material icon name (string) + * - component: Component path relative to controlcenter module (string) + */ + readonly property list panes: [ + QtObject { + readonly property string id: "network" + readonly property string label: "network" + readonly property string icon: "router" + readonly property string component: "network/NetworkingPane.qml" + }, + QtObject { + readonly property string id: "bluetooth" + readonly property string label: "bluetooth" + readonly property string icon: "settings_bluetooth" + readonly property string component: "bluetooth/BtPane.qml" + }, + QtObject { + readonly property string id: "audio" + readonly property string label: "audio" + readonly property string icon: "volume_up" + readonly property string component: "audio/AudioPane.qml" + }, + QtObject { + readonly property string id: "appearance" + readonly property string label: "appearance" + readonly property string icon: "palette" + readonly property string component: "appearance/AppearancePane.qml" + }, + QtObject { + readonly property string id: "taskbar" + readonly property string label: "taskbar" + readonly property string icon: "task_alt" + readonly property string component: "taskbar/TaskbarPane.qml" + }, + QtObject { + readonly property string id: "launcher" + readonly property string label: "launcher" + readonly property string icon: "apps" + readonly property string component: "launcher/LauncherPane.qml" + } + ] + + /** + * Get the count of registered panes + */ + readonly property int count: panes.length + + /** + * Get pane labels as a list of strings + * Useful for Session.qml's panes property + */ + readonly property var labels: { + const result = []; + for (let i = 0; i < panes.length; i++) { + result.push(panes[i].label); + } + return result; + } + + /** + * Get pane metadata by index + * @param index The index of the pane + * @return The pane metadata object or null if index is out of bounds + */ + function getByIndex(index: int): QtObject { + if (index >= 0 && index < panes.length) { + return panes[index]; + } + return null; + } + + /** + * Get pane index by label + * @param label The label to search for + * @return The index of the pane or -1 if not found + */ + function getIndexByLabel(label: string): int { + for (let i = 0; i < panes.length; i++) { + if (panes[i].label === label) { + return i; + } + } + return -1; + } + + /** + * Get pane metadata by label + * @param label The label to search for + * @return The pane metadata object or null if not found + */ + function getByLabel(label: string): QtObject { + const index = getIndexByLabel(label); + return getByIndex(index); + } + + /** + * Get pane metadata by id + * @param id The id to search for + * @return The pane metadata object or null if not found + */ + function getById(id: string): QtObject { + for (let i = 0; i < panes.length; i++) { + if (panes[i].id === id) { + return panes[i]; + } + } + return null; + } +} + diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 756d73a..b9256a9 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -9,6 +9,7 @@ import "launcher" import qs.components import qs.services import qs.config +import qs.modules.controlcenter import Quickshell.Widgets import QtQuick import QtQuick.Layouts @@ -76,45 +77,13 @@ ClippingRectangle { } } - Pane { - index: 0 - sourceComponent: NetworkingPane { - session: root.session - } - } - - Pane { - index: 1 - sourceComponent: BtPane { - session: root.session - } - } - - Pane { - index: 2 - sourceComponent: AudioPane { - session: root.session - } - } + Repeater { + model: PaneRegistry.count - Pane { - index: 3 - sourceComponent: AppearancePane { - session: root.session - } - } - - Pane { - index: 4 - sourceComponent: TaskbarPane { - session: root.session - } - } - - Pane { - index: 5 - sourceComponent: LauncherPane { - session: root.session + Pane { + required property int index + paneIndex: index + componentPath: PaneRegistry.getByIndex(index).component } } @@ -135,8 +104,8 @@ ClippingRectangle { component Pane: Item { id: pane - required property int index - property alias sourceComponent: loader.sourceComponent + required property int paneIndex + required property string componentPath implicitWidth: root.width implicitHeight: root.height @@ -146,7 +115,7 @@ ClippingRectangle { // Function to compute if this pane should be active function updateActive(): void { - const diff = Math.abs(root.session.activeIndex - pane.index); + const diff = Math.abs(root.session.activeIndex - pane.paneIndex); const isActivePane = diff === 0; let shouldBeActive = false; @@ -187,6 +156,13 @@ ClippingRectangle { if (active && !pane.hasBeenLoaded) { pane.hasBeenLoaded = true; } + + // Load the component with initial properties when activated + if (active && !item) { + loader.setSource(pane.componentPath, { + "session": root.session + }); + } } onItemChanged: { diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index 5d7b094..9c6a754 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -1,8 +1,9 @@ import QtQuick import "./state" +import qs.modules.controlcenter QtObject { - readonly property list panes: ["network", "bluetooth", "audio", "appearance", "taskbar", "launcher"] + readonly property list panes: PaneRegistry.labels required property var root property bool floating: false -- cgit v1.2.3-freya From e8fc13630c2fb67d75325e72ba66a811d3c1f4c9 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 17:20:00 -0500 Subject: refactor: SettingsHeader on all panels --- .../controlcenter/appearance/AppearancePane.qml | 16 +---- modules/controlcenter/audio/AudioPane.qml | 2 +- modules/controlcenter/bluetooth/Details.qml | 18 ++--- modules/controlcenter/bluetooth/Settings.qml | 16 ++--- .../controlcenter/components/SettingsHeader.qml | 59 ++++++++++++++++ modules/controlcenter/launcher/LauncherPane.qml | 81 +++++++++++----------- modules/controlcenter/launcher/Settings.qml | 16 ++--- modules/controlcenter/network/EthernetSettings.qml | 16 ++--- modules/controlcenter/network/NetworkSettings.qml | 16 ++--- modules/controlcenter/network/WirelessSettings.qml | 16 ++--- 10 files changed, 128 insertions(+), 128 deletions(-) create mode 100644 modules/controlcenter/components/SettingsHeader.qml diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 2041bf8..3ba0549 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -97,19 +97,9 @@ Item { 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 + SettingsHeader { + icon: "palette" + title: qsTr("Appearance Settings") } StyledText { diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 02cda5b..9b0c7d2 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -233,7 +233,7 @@ Item { anchors.top: parent.top spacing: Appearance.spacing.normal - ConnectionHeader { + SettingsHeader { icon: "volume_up" title: qsTr("Audio Settings") } diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index c9d10cd..5496966 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects @@ -41,20 +42,9 @@ StyledFlickable { anchors.top: parent.top spacing: Appearance.spacing.normal - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - animate: true - text: Icons.getBluetoothIcon(root.device?.icon ?? "") - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - animate: true - text: root.device?.name ?? "" - font.pointSize: Appearance.font.size.large - font.bold: true + SettingsHeader { + icon: Icons.getBluetoothIcon(root.device?.icon ?? "") + title: root.device?.name ?? "" } StyledText { diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index c8453b6..b3245ab 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects @@ -17,18 +18,9 @@ ColumnLayout { spacing: Appearance.spacing.normal - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "bluetooth" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Bluetooth Settings") - font.pointSize: Appearance.font.size.large - font.bold: true + SettingsHeader { + icon: "bluetooth" + title: qsTr("Bluetooth Settings") } StyledText { diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml new file mode 100644 index 0000000..9a77968 --- /dev/null +++ b/modules/controlcenter/components/SettingsHeader.qml @@ -0,0 +1,59 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.config +import QtQuick +import QtQuick.Layouts + +/** + * SettingsHeader + * + * Reusable header component for settings panes. Displays a large icon and title + * in a consistent format across all settings screens. + * + * Usage: + * ```qml + * SettingsHeader { + * icon: "router" + * title: qsTr("Network Settings") + * } + * ``` + */ +Item { + id: root + + /** + * Material icon name to display + */ + required property string icon + + /** + * Title text to display + */ + required property string title + + Layout.fillWidth: true + implicitHeight: column.implicitHeight + + ColumnLayout { + id: column + + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: root.icon + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: root.title + font.pointSize: Appearance.font.size.large + font.bold: true + } + } +} + diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index a8c5c76..cf965e8 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -509,53 +509,54 @@ Item { spacing: Appearance.spacing.normal - 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 - - Loader { - id: iconLoader - sourceComponent: parent.parent.displayedApp ? appIconComponent : defaultIconComponent + // Show SettingsHeader when no app is selected, or show app icon + title when app is selected + SettingsHeader { + Layout.leftMargin: Appearance.padding.large * 2 + Layout.rightMargin: Appearance.padding.large * 2 + Layout.topMargin: Appearance.padding.large * 2 + visible: displayedApp === null + icon: "apps" + title: qsTr("Launcher Applications") } - 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"); + // App icon and title display (shown when app is selected) + Item { + Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: Appearance.padding.large * 2 + Layout.rightMargin: Appearance.padding.large * 2 + Layout.topMargin: Appearance.padding.large * 2 + visible: displayedApp !== null + implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth) + implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight + + ColumnLayout { + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + IconImage { + id: appIconImage + Layout.alignment: Qt.AlignHCenter + implicitSize: Appearance.font.size.extraLarge * 3 * 2 + source: { + const app = appDetailsLayout.displayedApp; + if (!app) return "image-missing"; + const entry = app.entry; + if (entry && entry.icon) { + return Quickshell.iconPath(entry.icon, "image-missing"); + } + return "image-missing"; } - return "image-missing"; } - } - } - Component { - id: defaultIconComponent - MaterialIcon { - text: "apps" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true + StyledText { + id: appTitleText + Layout.alignment: Qt.AlignHCenter + text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" + 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 diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index 1fef7f5..161221e 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects @@ -16,18 +17,9 @@ ColumnLayout { spacing: Appearance.spacing.normal - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "apps" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Launcher Settings") - font.pointSize: Appearance.font.size.large - font.bold: true + SettingsHeader { + icon: "apps" + title: qsTr("Launcher Settings") } SectionHeader { diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml index 161492c..f0f66b4 100644 --- a/modules/controlcenter/network/EthernetSettings.qml +++ b/modules/controlcenter/network/EthernetSettings.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects @@ -16,18 +17,9 @@ ColumnLayout { spacing: Appearance.spacing.normal - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "cable" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Ethernet settings") - font.pointSize: Appearance.font.size.large - font.bold: true + SettingsHeader { + icon: "cable" + title: qsTr("Ethernet settings") } StyledText { diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index 75a7660..22e07cb 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects @@ -16,18 +17,9 @@ ColumnLayout { spacing: Appearance.spacing.normal - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "router" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Network Settings") - font.pointSize: Appearance.font.size.large - font.bold: true + SettingsHeader { + icon: "router" + title: qsTr("Network Settings") } SectionHeader { diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml index 0eb1578..f87fe39 100644 --- a/modules/controlcenter/network/WirelessSettings.qml +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects @@ -16,18 +17,9 @@ ColumnLayout { spacing: Appearance.spacing.normal - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "wifi" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Network settings") - font.pointSize: Appearance.font.size.large - font.bold: true + SettingsHeader { + icon: "wifi" + title: qsTr("Network settings") } SectionHeader { -- cgit v1.2.3-freya From 70ec8cea651c0f49e9ccf25b6e8685d81ac6710b Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 19:11:41 -0500 Subject: refactor: NetworkConnection util created, migrated all functions --- modules/bar/popouts/Network.qml | 34 +++--- modules/bar/popouts/WirelessPassword.qml | 3 +- modules/controlcenter/network/NetworkingPane.qml | 43 +------- modules/controlcenter/network/WirelessDetails.qml | 38 +------ modules/controlcenter/network/WirelessList.qml | 44 +------- .../network/WirelessPasswordDialog.qml | 3 +- utils/NetworkConnection.qml | 122 +++++++++++++++++++++ 7 files changed, 144 insertions(+), 143 deletions(-) create mode 100644 utils/NetworkConnection.qml diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index b9f66c4..0e99613 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -131,27 +131,19 @@ ColumnLayout { Nmcli.disconnectFromNetwork(); } else { root.connectingToSsid = networkItem.modelData.ssid; - // Check if network is secure - if (networkItem.modelData.isSecure) { - // Try to connect first - will show password dialog if password is needed - Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, result => { - if (result && result.needsPassword) { - // Password is required - show password dialog - root.passwordNetwork = networkItem.modelData; - root.showPasswordDialog = true; - root.wrapper.currentName = "wirelesspassword"; - } else if (result && result.success) { - // Connection successful with saved password - root.connectingToSsid = ""; - } else { - // Connection failed for other reasons - root.connectingToSsid = ""; - } - }); - } else { - // Open network, no password needed - Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); - } + NetworkConnection.handleConnect( + networkItem.modelData, + null, + (network) => { + // Password is required - show password dialog + root.passwordNetwork = network; + root.showPasswordDialog = true; + root.wrapper.currentName = "wirelesspassword"; + } + ); + + // Clear connecting state if connection succeeds immediately (saved profile) + // This is handled by the onActiveChanged connection below } } } diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index d91c87c..5da50b6 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -4,6 +4,7 @@ import qs.components import qs.components.controls import qs.services import qs.config +import qs.utils import Quickshell import QtQuick import QtQuick.Layouts @@ -452,7 +453,7 @@ ColumnLayout { text = qsTr("Connecting..."); // Connect to network - Nmcli.connectToNetwork(root.network.ssid, password, root.network.bssid || "", result => { + NetworkConnection.connectWithPassword(root.network, password, result => { if (result && result.success) // Connection successful, monitor will handle the rest {} else if (result && result.needsPassword) { diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 9a7a4e1..d76e8f5 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -415,7 +415,7 @@ Item { if (modelData && modelData.active) { Nmcli.disconnectFromNetwork(); } else if (modelData) { - handleWirelessConnect(modelData); + NetworkConnection.handleConnect(modelData, root.session, null); } } } @@ -586,46 +586,5 @@ Item { Nmcli.loadSavedConnections(() => {}); } } - - function handleWirelessConnect(network): void { - if (Nmcli.active && Nmcli.active.ssid !== network.ssid) { - Nmcli.disconnectFromNetwork(); - Qt.callLater(() => { - connectToWirelessNetwork(network); - }); - } else { - connectToWirelessNetwork(network); - } - } - - function connectToWirelessNetwork(network): void { - if (network.isSecure) { - const hasSavedProfile = Nmcli.hasSavedProfile(network.ssid); - - if (hasSavedProfile) { - Nmcli.connectToNetwork(network.ssid, "", network.bssid, null); - } else { - Nmcli.connectToNetworkWithPasswordCheck( - network.ssid, - network.isSecure, - (result) => { - if (result.needsPassword) { - if (Nmcli.pendingConnection) { - Nmcli.connectionCheckTimer.stop(); - Nmcli.immediateCheckTimer.stop(); - Nmcli.immediateCheckTimer.checkCount = 0; - Nmcli.pendingConnection = null; - } - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = network; - } - }, - network.bssid - ); - } - } else { - Nmcli.connectToNetwork(network.ssid, "", network.bssid, null); - } - } } diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 09abff3..57c06c8 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -8,6 +8,7 @@ import qs.components.effects import qs.components.containers import qs.services import qs.config +import qs.utils import QtQuick import QtQuick.Layouts @@ -125,7 +126,7 @@ Item { checked: root.network?.active ?? false toggle.onToggled: { if (checked) { - root.handleConnect(); + NetworkConnection.handleConnect(root.network, root.session, null); } else { Nmcli.disconnectFromNetwork(); } @@ -207,39 +208,4 @@ Item { } } - function handleConnect(): void { - if (Nmcli.active && Nmcli.active.ssid !== root.network.ssid) { - Nmcli.disconnectFromNetwork(); - Qt.callLater(() => { - connectToNetwork(); - }); - } else { - connectToNetwork(); - } - } - - function connectToNetwork(): void { - if (root.network.isSecure) { - const hasSavedProfile = Nmcli.hasSavedProfile(root.network.ssid); - - if (hasSavedProfile) { - Nmcli.connectToNetwork(root.network.ssid, "", root.network.bssid, null); - } else { - Nmcli.connectToNetworkWithPasswordCheck(root.network.ssid, root.network.isSecure, result => { - if (result.needsPassword) { - if (Nmcli.pendingConnection) { - Nmcli.connectionCheckTimer.stop(); - Nmcli.immediateCheckTimer.stop(); - Nmcli.immediateCheckTimer.checkCount = 0; - Nmcli.pendingConnection = null; - } - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = root.network; - } - }, root.network.bssid); - } - } else { - Nmcli.connectToNetwork(root.network.ssid, "", root.network.bssid, null); - } - } } diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 00af47a..18f728c 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -7,6 +7,7 @@ import qs.components.controls import qs.components.containers import qs.services import qs.config +import qs.utils import QtQuick import QtQuick.Layouts @@ -193,7 +194,7 @@ ColumnLayout { if (modelData.active) { Nmcli.disconnectFromNetwork(); } else { - handleConnect(modelData); + NetworkConnection.handleConnect(modelData, root.session, null); } } } @@ -217,45 +218,4 @@ ColumnLayout { Nmcli.loadSavedConnections(() => {}); } } - - function handleConnect(network): void { - if (Nmcli.active && Nmcli.active.ssid !== network.ssid) { - Nmcli.disconnectFromNetwork(); - Qt.callLater(() => { - connectToNetwork(network); - }); - } else { - connectToNetwork(network); - } - } - - function connectToNetwork(network): void { - if (network.isSecure) { - const hasSavedProfile = Nmcli.hasSavedProfile(network.ssid); - - if (hasSavedProfile) { - Nmcli.connectToNetwork(network.ssid, "", network.bssid, null); - } else { - Nmcli.connectToNetworkWithPasswordCheck( - network.ssid, - network.isSecure, - (result) => { - if (result.needsPassword) { - if (Nmcli.pendingConnection) { - Nmcli.connectionCheckTimer.stop(); - Nmcli.immediateCheckTimer.stop(); - Nmcli.immediateCheckTimer.checkCount = 0; - Nmcli.pendingConnection = null; - } - root.session.network.showPasswordDialog = true; - root.session.network.pendingNetwork = network; - } - }, - network.bssid - ); - } - } else { - Nmcli.connectToNetwork(network.ssid, "", network.bssid, null); - } - } } \ No newline at end of file diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 4b350be..0f1a5cd 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -8,6 +8,7 @@ import qs.components.effects import qs.components.containers import qs.services import qs.config +import qs.utils import Quickshell import QtQuick import QtQuick.Layouts @@ -391,7 +392,7 @@ Item { text = qsTr("Connecting..."); // Connect to network - Nmcli.connectToNetwork(root.network.ssid, password, root.network.bssid || "", result => { + NetworkConnection.connectWithPassword(root.network, password, result => { if (result && result.success) // Connection successful, monitor will handle the rest {} else if (result && result.needsPassword) { diff --git a/utils/NetworkConnection.qml b/utils/NetworkConnection.qml new file mode 100644 index 0000000..c7595b1 --- /dev/null +++ b/utils/NetworkConnection.qml @@ -0,0 +1,122 @@ +pragma Singleton + +import qs.services +import QtQuick + +/** + * NetworkConnection + * + * Centralized utility for network connection logic. Provides a single source of truth + * for connecting to wireless networks, eliminating code duplication across + * controlcenter components and bar popouts. + * + * Usage: + * ```qml + * import qs.utils + * + * // With Session object (controlcenter) + * NetworkConnection.handleConnect(network, session); + * + * // Without Session object (bar popouts) - provide password dialog callback + * NetworkConnection.handleConnect(network, null, (network) => { + * // Show password dialog + * root.passwordNetwork = network; + * root.showPasswordDialog = true; + * }); + * ``` + */ +QtObject { + id: root + + /** + * Handle network connection with automatic disconnection if needed. + * If there's an active network different from the target, disconnects first, + * then connects to the target network. + * + * @param network The network object to connect to (must have ssid property) + * @param session Optional Session object (for controlcenter - must have network property with showPasswordDialog and pendingNetwork) + * @param onPasswordNeeded Optional callback function(network) called when password is needed (for bar popouts) + */ + function handleConnect(network, session, onPasswordNeeded): void { + if (!network) { + return; + } + + if (Nmcli.active && Nmcli.active.ssid !== network.ssid) { + Nmcli.disconnectFromNetwork(); + Qt.callLater(() => { + root.connectToNetwork(network, session, onPasswordNeeded); + }); + } else { + root.connectToNetwork(network, session, onPasswordNeeded); + } + } + + /** + * Connect to a wireless network. + * Handles both secured and open networks, checks for saved profiles, + * and shows password dialog if needed. + * + * @param network The network object to connect to (must have ssid, isSecure, bssid properties) + * @param session Optional Session object (for controlcenter - must have network property with showPasswordDialog and pendingNetwork) + * @param onPasswordNeeded Optional callback function(network) called when password is needed (for bar popouts) + */ + function connectToNetwork(network, session, onPasswordNeeded): void { + if (!network) { + return; + } + + if (network.isSecure) { + const hasSavedProfile = Nmcli.hasSavedProfile(network.ssid); + + if (hasSavedProfile) { + Nmcli.connectToNetwork(network.ssid, "", network.bssid, null); + } else { + // Use password check with callback + Nmcli.connectToNetworkWithPasswordCheck( + network.ssid, + network.isSecure, + (result) => { + if (result.needsPassword) { + // Clear pending connection if exists + if (Nmcli.pendingConnection) { + Nmcli.connectionCheckTimer.stop(); + Nmcli.immediateCheckTimer.stop(); + Nmcli.immediateCheckTimer.checkCount = 0; + Nmcli.pendingConnection = null; + } + + // Handle password dialog - use session if available, otherwise use callback + if (session && session.network) { + session.network.showPasswordDialog = true; + session.network.pendingNetwork = network; + } else if (onPasswordNeeded) { + onPasswordNeeded(network); + } + } + }, + network.bssid + ); + } + } else { + Nmcli.connectToNetwork(network.ssid, "", network.bssid, null); + } + } + + /** + * Connect to a wireless network with a provided password. + * Used by password dialogs when the user has already entered a password. + * + * @param network The network object to connect to (must have ssid, bssid properties) + * @param password The password to use for connection + * @param onResult Optional callback function(result) called with connection result + */ + function connectWithPassword(network, password, onResult): void { + if (!network) { + return; + } + + Nmcli.connectToNetwork(network.ssid, password || "", network.bssid || "", onResult || null); + } +} + -- cgit v1.2.3-freya From 2d26626643e447b5c9f6f78619e8472bc35ea599 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 19:25:26 -0500 Subject: refactor: SplitPaneWithDetails integrated --- modules/controlcenter/bluetooth/BtPane.qml | 84 +++---------- .../components/SplitPaneWithDetails.qml | 130 ++++++++++++++++++++ modules/controlcenter/network/EthernetPane.qml | 121 ++++--------------- modules/controlcenter/network/WirelessPane.qml | 131 ++++----------------- 4 files changed, 190 insertions(+), 276 deletions(-) create mode 100644 modules/controlcenter/components/SplitPaneWithDetails.qml diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index c20f14b..3cd6ef9 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -2,87 +2,39 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import qs.components import qs.components.controls -import qs.components.effects import qs.components.containers import qs.config import Quickshell.Widgets import Quickshell.Bluetooth import QtQuick -import QtQuick.Layouts -Item { +SplitPaneWithDetails { id: root required property Session session anchors.fill: parent - SplitPaneLayout { - anchors.fill: parent + activeItem: session.bt.active + paneIdGenerator: function(item) { + return item ? (item.address || "") : ""; + } - leftContent: Component { - DeviceList { - anchors.fill: parent - session: root.session - } + leftContent: Component { + DeviceList { + session: root.session } + } - rightContent: Component { - Item { - id: rightBtPane - - property BluetoothDevice pane: root.session.bt.active - property string paneId: pane ? (pane.address || "") : "" - property Component targetComponent: settings - property Component nextComponent: settings - - function getComponentForPane() { - return pane ? details : settings; - } - - Component.onCompleted: { - targetComponent = getComponentForPane(); - nextComponent = targetComponent; - } - - Loader { - id: rightLoader - - anchors.fill: parent - - asynchronous: true - sourceComponent: rightBtPane.targetComponent - } - - Behavior on paneId { - PaneTransition { - target: rightLoader - propertyActions: [ - PropertyAction { - target: rightBtPane - property: "targetComponent" - value: rightBtPane.nextComponent - } - ] - } - } - - Connections { - target: root.session.bt - function onActiveChanged() { - rightBtPane.pane = root.session.bt.active; - rightBtPane.nextComponent = rightBtPane.getComponentForPane(); - rightBtPane.paneId = pane ? (pane.address || "") : ""; - } - } - } + rightDetailsComponent: Component { + Details { + session: root.session } } - Component { - id: settings - + rightSettingsComponent: Component { StyledFlickable { id: settingsFlickable flickableDirection: Flickable.VerticalFlick @@ -102,12 +54,4 @@ Item { } } } - - Component { - id: details - - Details { - session: root.session - } - } } diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml new file mode 100644 index 0000000..e139aca --- /dev/null +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -0,0 +1,130 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.effects +import qs.components.containers +import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +/** + * SplitPaneWithDetails + * + * A reusable component that provides a split-pane layout with a list on the left + * and a details/settings view on the right. The right pane automatically switches + * between details and settings views based on whether an item is selected. + * + * This component eliminates duplication across WirelessPane, EthernetPane, and BtPane + * by providing a standardized pattern for split-pane layouts with transition animations. + * + * Usage: + * ```qml + * SplitPaneWithDetails { + * activeItem: root.session.network.active + * leftContent: Component { + * WirelessList { + * session: root.session + * } + * } + * rightDetailsComponent: Component { + * WirelessDetails { + * session: root.session + * } + * } + * rightSettingsComponent: Component { + * StyledFlickable { + * WirelessSettings { + * session: root.session + * } + * } + * } + * paneIdGenerator: (item) => item ? (item.ssid || item.bssid || "") : "" + * } + * ``` + */ +Item { + id: root + + required property Component leftContent + required property Component rightDetailsComponent + required property Component rightSettingsComponent + + property var activeItem: null + property var paneIdGenerator: function(item) { return item ? String(item) : ""; } + + // Optional: Additional component to overlay on top (e.g., password dialogs) + property Component overlayComponent: null + + SplitPaneLayout { + id: splitLayout + + anchors.fill: parent + + leftContent: root.leftContent + + rightContent: Component { + Item { + id: rightPaneItem + + property var pane: root.activeItem + property string paneId: root.paneIdGenerator(pane) + property Component targetComponent: root.rightSettingsComponent + property Component nextComponent: root.rightSettingsComponent + + function getComponentForPane() { + return pane ? root.rightDetailsComponent : root.rightSettingsComponent; + } + + Component.onCompleted: { + targetComponent = getComponentForPane(); + nextComponent = targetComponent; + } + + Loader { + id: rightLoader + + anchors.fill: parent + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + + clip: false + asynchronous: true + sourceComponent: rightPaneItem.targetComponent + } + + Behavior on paneId { + PaneTransition { + target: rightLoader + propertyActions: [ + PropertyAction { + target: rightPaneItem + property: "targetComponent" + value: rightPaneItem.nextComponent + } + ] + } + } + + onPaneChanged: { + nextComponent = getComponentForPane(); + paneId = root.paneIdGenerator(pane); + } + } + } + } + + // Overlay component (e.g., password dialogs) + Loader { + id: overlayLoader + + anchors.fill: parent + z: 1000 + sourceComponent: root.overlayComponent + active: root.overlayComponent !== null + } +} + diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml index fc979c3..126535a 100644 --- a/modules/controlcenter/network/EthernetPane.qml +++ b/modules/controlcenter/network/EthernetPane.qml @@ -1,131 +1,50 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components -import qs.components.effects import qs.components.containers import qs.config import Quickshell.Widgets import QtQuick -import QtQuick.Layouts -RowLayout { +SplitPaneWithDetails { id: root required property Session session anchors.fill: parent - spacing: 0 - - Item { - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true + activeItem: session.ethernet.active + paneIdGenerator: function(item) { + return item ? (item.interface || "") : ""; + } + leftContent: Component { EthernetList { - 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 - session: root.session } + } - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 + rightDetailsComponent: Component { + EthernetDetails { + session: root.session } } - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: rightBorder.innerRadius - color: "transparent" + rightSettingsComponent: Component { + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height clip: true - Loader { - id: loader - - property var pane: root.session.ethernet.active - property string paneId: pane ? (pane.interface || "") : "" - property Component targetComponent: settings - property Component nextComponent: settings - - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - - opacity: 1 - scale: 1 - transformOrigin: Item.Center - - clip: true - asynchronous: true - sourceComponent: loader.targetComponent - - Component.onCompleted: { - targetComponent = pane ? details : settings; - nextComponent = targetComponent; - } - - Behavior on paneId { - PaneTransition { - target: loader - propertyActions: [ - PropertyAction { - target: loader - property: "targetComponent" - value: loader.nextComponent - } - ] - } - } - - onPaneChanged: { - nextComponent = pane ? details : settings; - paneId = pane ? (pane.interface || "") : ""; - } - } - } - - InnerBorder { - id: rightBorder - - leftThickness: Appearance.padding.normal / 2 - } - - Component { - id: settings - - StyledFlickable { - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - clip: true - - EthernetSettings { - id: settingsInner - - anchors.left: parent.left - anchors.right: parent.right - session: root.session - } - } - } - - Component { - id: details + EthernetSettings { + id: settingsInner - EthernetDetails { + anchors.left: parent.left + anchors.right: parent.right session: root.session } } } -} \ No newline at end of file +} diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index 0c8f0ae..109d416 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -1,136 +1,57 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components -import qs.components.effects import qs.components.containers import qs.config import Quickshell.Widgets import QtQuick -import QtQuick.Layouts -RowLayout { +SplitPaneWithDetails { id: root required property Session session anchors.fill: parent - spacing: 0 - - Item { - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true + activeItem: session.network.active + paneIdGenerator: function(item) { + return item ? (item.ssid || item.bssid || "") : ""; + } + leftContent: Component { WirelessList { - 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 - session: root.session } - - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } } - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - 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 var pane: root.session.network.active - property string paneId: pane ? (pane.ssid || pane.bssid || "") : "" - property Component targetComponent: settings - property Component nextComponent: settings - - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - - opacity: 1 - scale: 1 - transformOrigin: Item.Center - - clip: false - asynchronous: true - sourceComponent: loader.targetComponent - - Component.onCompleted: { - targetComponent = pane ? details : settings; - nextComponent = targetComponent; - } - - Behavior on paneId { - PaneTransition { - target: loader - propertyActions: [ - PropertyAction { - target: loader - property: "targetComponent" - value: loader.nextComponent - } - ] - } - } - - onPaneChanged: { - nextComponent = pane ? details : settings; - paneId = pane ? (pane.ssid || pane.bssid || "") : ""; - } - } - } - - InnerBorder { - id: rightBorder - - leftThickness: Appearance.padding.normal / 2 + rightDetailsComponent: Component { + WirelessDetails { + session: root.session } + } - Component { - id: settings - - StyledFlickable { - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - clip: true - - WirelessSettings { - id: settingsInner - - anchors.left: parent.left - anchors.right: parent.right - session: root.session - } - } - } + rightSettingsComponent: Component { + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + clip: true - Component { - id: details + WirelessSettings { + id: settingsInner - WirelessDetails { + anchors.left: parent.left + anchors.right: parent.right session: root.session } } } - WirelessPasswordDialog { - anchors.fill: parent - session: root.session - z: 1000 + overlayComponent: Component { + WirelessPasswordDialog { + anchors.fill: parent + session: root.session + } } -} \ No newline at end of file +} -- cgit v1.2.3-freya From ff4e9bbdfd13077f94e1a30ac1dd7e7df2238438 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 19:51:28 -0500 Subject: refactor: Updated Bluetooth, Ethernet, and Wireless lists to use DeviceList component and improved layout structure --- modules/controlcenter/bluetooth/BtPane.qml | 1 + modules/controlcenter/bluetooth/DeviceList.qml | 293 ++++++++------------- modules/controlcenter/components/DeviceDetails.qml | 108 ++++++++ modules/controlcenter/components/DeviceList.qml | 125 +++++++++ modules/controlcenter/network/EthernetList.qml | 93 +++---- modules/controlcenter/network/WirelessList.qml | 152 +++++------ 6 files changed, 446 insertions(+), 326 deletions(-) create mode 100644 modules/controlcenter/components/DeviceDetails.qml create mode 100644 modules/controlcenter/components/DeviceList.qml diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 3cd6ef9..6877801 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import "." import qs.components import qs.components.controls import qs.components.containers diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index 06700e8..b3db236 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.containers @@ -12,165 +13,144 @@ import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts -ColumnLayout { +DeviceList { id: root required property Session session readonly property bool smallDiscoverable: width <= 540 readonly property bool smallPairable: width <= 480 - spacing: Appearance.spacing.small + title: qsTr("Devices (%1)").arg(Bluetooth.devices.values.length) + description: qsTr("All available bluetooth devices") + activeItem: session.bt.active - RowLayout { - spacing: Appearance.spacing.smaller + model: ScriptModel { + id: deviceModel - StyledText { - text: qsTr("Bluetooth") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } - - ToggleButton { - toggled: Bluetooth.defaultAdapter?.enabled ?? false - icon: "power" - accent: "Tertiary" - - function onClicked(): void { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.enabled = !adapter.enabled; - } - } - - ToggleButton { - toggled: Bluetooth.defaultAdapter?.discoverable ?? false - icon: root.smallDiscoverable ? "group_search" : "" - label: root.smallDiscoverable ? "" : qsTr("Discoverable") - - function onClicked(): void { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.discoverable = !adapter.discoverable; - } - } - - ToggleButton { - toggled: Bluetooth.defaultAdapter?.pairable ?? false - icon: "missing_controller" - label: root.smallPairable ? "" : qsTr("Pairable") - - function onClicked(): void { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.pairable = !adapter.pairable; - } - } - - ToggleButton { - toggled: !root.session.bt.active - icon: "settings" - accent: "Primary" - - function onClicked(): void { - if (root.session.bt.active) - root.session.bt.active = null; - else { - root.session.bt.active = deviceModel.values[0] ?? null; - } - } - } + values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired)) } - RowLayout { - Layout.topMargin: Appearance.spacing.large - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + headerComponent: Component { + RowLayout { + spacing: Appearance.spacing.smaller StyledText { - Layout.fillWidth: true - text: qsTr("Devices (%1)").arg(Bluetooth.devices.values.length) - font.pointSize: Appearance.font.size.normal + text: qsTr("Bluetooth") + font.pointSize: Appearance.font.size.large font.weight: 500 } - StyledText { + Item { Layout.fillWidth: true - text: qsTr("All available bluetooth devices") - color: Colours.palette.m3outline } - } - StyledRect { - implicitWidth: implicitHeight - implicitHeight: scanIcon.implicitHeight + Appearance.padding.normal * 2 + ToggleButton { + toggled: Bluetooth.defaultAdapter?.enabled ?? false + icon: "power" + accent: "Tertiary" - radius: Bluetooth.defaultAdapter?.discovering ? Appearance.rounding.normal : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) - color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + function onClicked(): void { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.enabled = !adapter.enabled; + } + } - StateLayer { - color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + ToggleButton { + toggled: Bluetooth.defaultAdapter?.discoverable ?? false + icon: root.smallDiscoverable ? "group_search" : "" + label: root.smallDiscoverable ? "" : qsTr("Discoverable") function onClicked(): void { const adapter = Bluetooth.defaultAdapter; if (adapter) - adapter.discovering = !adapter.discovering; + adapter.discoverable = !adapter.discoverable; } } - MaterialIcon { - id: scanIcon + ToggleButton { + toggled: Bluetooth.defaultAdapter?.pairable ?? false + icon: "missing_controller" + label: root.smallPairable ? "" : qsTr("Pairable") - anchors.centerIn: parent - animate: true - text: "bluetooth_searching" - color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - fill: Bluetooth.defaultAdapter?.discovering ? 1 : 0 + function onClicked(): void { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.pairable = !adapter.pairable; + } } - Behavior on radius { - Anim {} + ToggleButton { + toggled: !root.session.bt.active + icon: "settings" + accent: "Primary" + + function onClicked(): void { + if (root.session.bt.active) + root.session.bt.active = null; + else { + root.session.bt.active = root.model.values[0] ?? null; + } + } } } } - StyledListView { - id: view + titleSuffix: Component { + RowLayout { + spacing: Appearance.spacing.normal + + Item { + Layout.fillWidth: true + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: scanIcon.implicitHeight + Appearance.padding.normal * 2 - model: ScriptModel { - id: deviceModel + radius: Bluetooth.defaultAdapter?.discovering ? Appearance.rounding.normal : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired)) - } + StateLayer { + color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + + function onClicked(): void { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.discovering = !adapter.discovering; + } + } - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - spacing: Appearance.spacing.small / 2 + MaterialIcon { + id: scanIcon - StyledScrollBar.vertical: StyledScrollBar { - flickable: view + anchors.centerIn: parent + animate: true + text: "bluetooth_searching" + color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + fill: Bluetooth.defaultAdapter?.discovering ? 1 : 0 + } + + Behavior on radius { + Anim {} + } + } } + } - delegate: StyledRect { + delegate: Component { + StyledRect { id: device required property BluetoothDevice modelData readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting) readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected - anchors.left: view.contentItem.left - anchors.right: view.contentItem.right + anchors.left: parent.left + anchors.right: parent.right implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2 - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.bt.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal StateLayer { @@ -257,8 +237,18 @@ ColumnLayout { disabled: device.loading function onClicked(): void { - if (device.modelData) - device.modelData.connected = !device.modelData.connected; + if (device.loading) + return; + + if (device.connected) { + device.modelData.connected = false; + } else { + if (device.modelData.bonded) { + device.modelData.connected = true; + } else { + device.modelData.pair(); + } + } } } @@ -267,7 +257,7 @@ ColumnLayout { anchors.centerIn: parent animate: true - text: (device.modelData && device.modelData.connected) ? "link_off" : "link" + text: device.connected ? "link_off" : "link" color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface opacity: device.loading ? 0 : 1 @@ -281,78 +271,7 @@ ColumnLayout { } } - component ToggleButton: StyledRect { - id: toggleBtn - - required property bool toggled - property string icon - property string label - property string accent: "Secondary" - - function onClicked(): void { - } - - Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) - implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2 - implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2 - - radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) - color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] - - StateLayer { - id: toggleStateLayer - - color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] - - function onClicked(): void { - toggleBtn.onClicked(); - } - } - - RowLayout { - id: toggleBtnInner - - anchors.centerIn: parent - spacing: Appearance.spacing.normal - - MaterialIcon { - id: toggleBtnIcon - - visible: !!text - fill: toggleBtn.toggled ? 1 : 0 - text: toggleBtn.icon - color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] - font.pointSize: Appearance.font.size.large - - Behavior on fill { - Anim {} - } - } - - Loader { - asynchronous: true - active: !!toggleBtn.label - visible: active - - sourceComponent: StyledText { - text: toggleBtn.label - color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] - } - } - } - - Behavior on radius { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } - - Behavior on Layout.preferredWidth { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } + onItemSelected: function(item) { + session.bt.active = item; } } diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml new file mode 100644 index 0000000..256e689 --- /dev/null +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -0,0 +1,108 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.config +import QtQuick +import QtQuick.Layouts + +/** + * DeviceDetails + * + * A reusable base component for displaying device/network details with a standardized + * structure. Provides a header, connection status section, and flexible sections for + * device-specific information. + * + * This component eliminates duplication across WirelessDetails, EthernetDetails, and Bluetooth Details + * by providing a common structure while allowing full customization of sections. + * + * Usage: + * ```qml + * DeviceDetails { + * session: root.session + * device: session.network.active + * headerComponent: Component { + * ConnectionHeader { + * icon: "wifi" + * title: device?.ssid ?? "" + * } + * } + * sections: [ + * Component { + * // Connection status section + * }, + * Component { + * // Properties section + * } + * ] + * } + * ``` + */ +Item { + id: root + + required property Session session + property var device: null + + property Component headerComponent: null + property list sections: [] + + // Optional: Custom content to insert after header but before sections + property Component topContent: null + + // Optional: Custom content to insert after all sections + property Component bottomContent: null + + implicitWidth: layout.implicitWidth + implicitHeight: layout.implicitHeight + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal + + // Header component (e.g., ConnectionHeader or SettingsHeader) + Loader { + id: headerLoader + + Layout.fillWidth: true + sourceComponent: root.headerComponent + visible: root.headerComponent !== null + } + + // Top content (optional) + Loader { + id: topContentLoader + + Layout.fillWidth: true + sourceComponent: root.topContent + visible: root.topContent !== null + } + + // Sections + Repeater { + model: root.sections + + Loader { + Layout.fillWidth: true + sourceComponent: modelData + } + } + + // Bottom content (optional) + Loader { + id: bottomContentLoader + + Layout.fillWidth: true + sourceComponent: root.bottomContent + visible: root.bottomContent !== null + } + } +} + diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml new file mode 100644 index 0000000..f8473ff --- /dev/null +++ b/modules/controlcenter/components/DeviceList.qml @@ -0,0 +1,125 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +/** + * DeviceList + * + * A reusable base component for displaying lists of devices/networks with a standardized + * structure. Provides a header with action buttons, title/subtitle, and a scrollable list + * with customizable delegates. + * + * This component eliminates duplication across WirelessList, EthernetList, and Bluetooth DeviceList + * by providing a common structure while allowing full customization of headers and delegates. + * + * Usage: + * ```qml + * DeviceList { + * session: root.session + * title: qsTr("Networks (%1)").arg(Nmcli.networks.length) + * description: qsTr("All available WiFi networks") + * model: ScriptModel { + * values: [...Nmcli.networks].sort(...) + * } + * activeItem: session.network.active + * onItemSelected: (item) => { + * session.network.active = item; + * } + * headerComponent: Component { + * RowLayout { + * // Custom header buttons + * } + * } + * delegate: Component { + * // Custom delegate for each item + * } + * } + * ``` + */ +ColumnLayout { + id: root + + property Session session: null + property var model: null + property Component delegate: null + + property string title: "" + property string description: "" + property var activeItem: null + property Component headerComponent: null + property Component titleSuffix: null + + signal itemSelected(var item) + + spacing: Appearance.spacing.small + + // Header with action buttons (optional) + Loader { + id: headerLoader + + Layout.fillWidth: true + sourceComponent: root.headerComponent + visible: root.headerComponent !== null + } + + // Title and description row + RowLayout { + Layout.fillWidth: true + Layout.topMargin: root.headerComponent ? 0 : 0 + spacing: Appearance.spacing.small + visible: root.title !== "" || root.description !== "" + + StyledText { + visible: root.title !== "" + text: root.title + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Loader { + sourceComponent: root.titleSuffix + visible: root.titleSuffix !== null + } + + Item { + Layout.fillWidth: true + } + } + + // Expose view for access from parent components + property alias view: view + + // Description text + StyledText { + visible: root.description !== "" + Layout.fillWidth: true + text: root.description + color: Colours.palette.m3outline + } + + // List view + StyledListView { + id: view + + Layout.fillWidth: true + Layout.fillHeight: true + + model: root.model + delegate: root.delegate + + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: view + } + } +} + diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index 45c9481..03bd37e 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.containers @@ -9,81 +10,57 @@ import qs.config import QtQuick import QtQuick.Layouts -ColumnLayout { +DeviceList { id: root required property Session session - spacing: Appearance.spacing.small + title: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) + description: qsTr("All available ethernet devices") + activeItem: session.ethernet.active - RowLayout { - spacing: Appearance.spacing.smaller + model: Nmcli.ethernetDevices - StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } + headerComponent: Component { + RowLayout { + spacing: Appearance.spacing.smaller - ToggleButton { - toggled: !root.session.ethernet.active - icon: "settings" - accent: "Primary" + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } - onClicked: { - if (root.session.ethernet.active) - root.session.ethernet.active = null; - else { - root.session.ethernet.active = view.model.get(0)?.modelData ?? null; - } + Item { + Layout.fillWidth: true } - } - } - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + ToggleButton { + toggled: !root.session.ethernet.active + icon: "settings" + accent: "Primary" - StyledText { - text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) - font.pointSize: Appearance.font.size.large - font.weight: 500 + onClicked: { + if (root.session.ethernet.active) + root.session.ethernet.active = null; + else { + root.session.ethernet.active = root.view.model.get(0)?.modelData ?? null; + } + } + } } } - StyledText { - text: qsTr("All available ethernet devices") - color: Colours.palette.m3outline - } - - StyledListView { - id: view - - Layout.fillWidth: true - Layout.fillHeight: true - - model: Nmcli.ethernetDevices - - spacing: Appearance.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: view - } - - delegate: StyledRect { + delegate: Component { + StyledRect { required property var modelData anchors.left: parent.left anchors.right: parent.right - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal - border.width: root.session.ethernet.active === modelData ? 1 : 0 + border.width: root.activeItem === modelData ? 1 : 0 border.color: Colours.palette.m3primary StateLayer { @@ -165,4 +142,8 @@ ColumnLayout { implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } -} \ No newline at end of file + + onItemSelected: function(item) { + session.ethernet.active = item; + } +} diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 18f728c..2f0288f 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import "." import qs.components import qs.components.controls @@ -11,116 +12,94 @@ import qs.utils import QtQuick import QtQuick.Layouts -ColumnLayout { +DeviceList { id: root required property Session session - spacing: Appearance.spacing.small - - RowLayout { - spacing: Appearance.spacing.smaller - + title: qsTr("Networks (%1)").arg(Nmcli.networks.length) + description: qsTr("All available WiFi networks") + activeItem: session.network.active + + titleSuffix: Component { StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 + visible: Nmcli.scanning + text: qsTr("Scanning...") + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.small } + } - Item { - Layout.fillWidth: true - } + model: ScriptModel { + values: [...Nmcli.networks].sort((a, b) => { + // Put active/connected network first + if (a.active !== b.active) + return b.active - a.active; + // Then sort by signal strength + return b.strength - a.strength; + }) + } - ToggleButton { - toggled: Nmcli.wifiEnabled - icon: "wifi" - accent: "Tertiary" + headerComponent: Component { + RowLayout { + spacing: Appearance.spacing.smaller - onClicked: { - Nmcli.toggleWifi(null); + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 } - } - ToggleButton { - toggled: Nmcli.scanning - icon: "wifi_find" - accent: "Secondary" - - onClicked: { - Nmcli.rescanWifi(); + Item { + Layout.fillWidth: true } - } - ToggleButton { - toggled: !root.session.network.active - icon: "settings" - accent: "Primary" + ToggleButton { + toggled: Nmcli.wifiEnabled + icon: "wifi" + accent: "Tertiary" - onClicked: { - if (root.session.network.active) - root.session.network.active = null; - else { - root.session.network.active = view.model.get(0)?.modelData ?? null; + onClicked: { + Nmcli.toggleWifi(null); } } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - StyledText { - text: qsTr("Networks (%1)").arg(Nmcli.networks.length) - font.pointSize: Appearance.font.size.large - font.weight: 500 - } + ToggleButton { + toggled: Nmcli.scanning + icon: "wifi_find" + accent: "Secondary" - StyledText { - visible: Nmcli.scanning - text: qsTr("Scanning...") - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.small - } - } - - StyledText { - text: qsTr("All available WiFi networks") - color: Colours.palette.m3outline - } - - StyledListView { - id: view - - Layout.fillWidth: true - Layout.fillHeight: true - - model: ScriptModel { - values: [...Nmcli.networks].sort((a, b) => { - // Put active/connected network first - if (a.active !== b.active) - return b.active - a.active; - // Then sort by signal strength - return b.strength - a.strength; - }) - } + onClicked: { + Nmcli.rescanWifi(); + } + } - spacing: Appearance.spacing.small / 2 - clip: true + ToggleButton { + toggled: !root.session.network.active + icon: "settings" + accent: "Primary" - StyledScrollBar.vertical: StyledScrollBar { - flickable: view + onClicked: { + if (root.session.network.active) + root.session.network.active = null; + else { + root.session.network.active = root.view.model.get(0)?.modelData ?? null; + } + } + } } + } - delegate: StyledRect { + delegate: Component { + StyledRect { required property var modelData anchors.left: parent.left anchors.right: parent.right - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.network.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal - border.width: root.session.network.active === modelData ? 1 : 0 + border.width: root.activeItem === modelData ? 1 : 0 border.color: Colours.palette.m3primary StateLayer { @@ -213,9 +192,16 @@ ColumnLayout { } } + onItemSelected: function(item) { + session.network.active = item; + if (item && item.ssid) { + checkSavedProfileForNetwork(item.ssid); + } + } + function checkSavedProfileForNetwork(ssid: string): void { if (ssid && ssid.length > 0) { Nmcli.loadSavedConnections(() => {}); } } -} \ No newline at end of file +} -- cgit v1.2.3-freya From ef46a02b2f3561574c1ada5afefbbb806bcb6a3b Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 21:04:32 -0500 Subject: refactor: ToggleButton Tooltips and DeviceList --- components/controls/ToggleButton.qml | 51 +- components/controls/Tooltip.qml | 170 +++++ modules/controlcenter/bluetooth/BtPane.qml | 19 +- modules/controlcenter/bluetooth/Details.qml | 681 +++++++++++---------- modules/controlcenter/bluetooth/DeviceList.qml | 84 ++- modules/controlcenter/components/DeviceDetails.qml | 2 + modules/controlcenter/components/DeviceList.qml | 14 +- modules/controlcenter/launcher/LauncherPane.qml | 4 + modules/controlcenter/network/EthernetDetails.qml | 140 +++-- modules/controlcenter/network/EthernetList.qml | 6 +- modules/controlcenter/network/NetworkingPane.qml | 377 ++---------- modules/controlcenter/network/WirelessDetails.qml | 182 +++--- modules/controlcenter/network/WirelessList.qml | 13 +- 13 files changed, 865 insertions(+), 878 deletions(-) create mode 100644 components/controls/Tooltip.qml diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index 9d8e094..b2c2afe 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -1,5 +1,6 @@ import ".." import qs.components +import qs.components.controls import qs.components.effects import qs.services import qs.config @@ -13,12 +14,31 @@ StyledRect { property string icon property string label property string accent: "Secondary" + property real iconSize: Appearance.font.size.large + property real horizontalPadding: Appearance.padding.large + property real verticalPadding: Appearance.padding.normal + property string tooltip: "" + property bool hovered: false signal clicked + Component.onCompleted: { + hovered = toggleStateLayer.containsMouse; + } + + Connections { + target: toggleStateLayer + function onContainsMouseChanged() { + const newHovered = toggleStateLayer.containsMouse; + if (hovered !== newHovered) { + hovered = newHovered; + } + } + } + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) - implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2 - implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 + implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] @@ -46,7 +66,7 @@ StyledRect { fill: root.toggled ? 1 : 0 text: root.icon color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] - font.pointSize: Appearance.font.size.large + font.pointSize: root.iconSize Behavior on fill { Anim {} @@ -78,5 +98,30 @@ StyledRect { easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } + + // Tooltip - positioned absolutely, doesn't affect layout + Loader { + id: tooltipLoader + active: root.tooltip !== "" + asynchronous: true + z: 10000 + width: 0 + height: 0 + sourceComponent: Component { + Tooltip { + target: root + text: root.tooltip + } + } + // Completely remove from layout + Layout.fillWidth: false + Layout.fillHeight: false + Layout.preferredWidth: 0 + Layout.preferredHeight: 0 + Layout.maximumWidth: 0 + Layout.maximumHeight: 0 + Layout.minimumWidth: 0 + Layout.minimumHeight: 0 + } } diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml new file mode 100644 index 0000000..bab8086 --- /dev/null +++ b/components/controls/Tooltip.qml @@ -0,0 +1,170 @@ +import ".." +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Popup { + id: root + + required property Item target + required property string text + property int delay: 500 + property int timeout: 0 + + property bool tooltipVisible: false + property Timer showTimer: Timer { + interval: root.delay + onTriggered: root.tooltipVisible = true + } + property Timer hideTimer: Timer { + interval: root.timeout + onTriggered: root.tooltipVisible = false + } + + // Popup properties - doesn't affect layout + parent: { + let p = target; + // Walk up to find the root Item (usually has anchors.fill: parent) + while (p && p.parent) { + const parentItem = p.parent; + // Check if this looks like a root pane Item + if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) { + return parentItem; + } + p = parentItem; + } + // Fallback + return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target; + } + + visible: tooltipVisible + modal: false + closePolicy: Popup.NoAutoClose + padding: 0 + margins: 0 + + // Update position when target moves or tooltip becomes visible + onTooltipVisibleChanged: { + if (tooltipVisible) { + Qt.callLater(updatePosition); + } + } + Connections { + target: root.target + function onXChanged() { if (root.tooltipVisible) root.updatePosition(); } + function onYChanged() { if (root.tooltipVisible) root.updatePosition(); } + function onWidthChanged() { if (root.tooltipVisible) root.updatePosition(); } + function onHeightChanged() { if (root.tooltipVisible) root.updatePosition(); } + } + + function updatePosition() { + if (!target || !parent) return; + + // Wait for tooltipRect to have its size calculated + Qt.callLater(() => { + if (!target || !parent || !tooltipRect) return; + + // Get target position in parent's coordinate system + const targetPos = target.mapToItem(parent, 0, 0); + const targetCenterX = targetPos.x + target.width / 2; + + // Get tooltip size (use width/height if available, otherwise implicit) + const tooltipWidth = tooltipRect.width > 0 ? tooltipRect.width : tooltipRect.implicitWidth; + const tooltipHeight = tooltipRect.height > 0 ? tooltipRect.height : tooltipRect.implicitHeight; + + // Center tooltip horizontally on target + let newX = targetCenterX - tooltipWidth / 2; + + // Position tooltip above target + let newY = targetPos.y - tooltipHeight - Appearance.spacing.small; + + // Keep within bounds + const padding = Appearance.padding.normal; + if (newX < padding) { + newX = padding; + } else if (newX + tooltipWidth > (parent.width - padding)) { + newX = parent.width - tooltipWidth - padding; + } + + // Update popup position + x = newX; + y = newY; + }); + } + + enter: Transition { + Anim { + property: "opacity" + from: 0 + to: 1 + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + exit: Transition { + Anim { + property: "opacity" + from: 1 + to: 0 + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + // Monitor hover state + Connections { + target: root.target + function onHoveredChanged() { + if (target.hovered) { + showTimer.start(); + if (timeout > 0) { + hideTimer.stop(); + hideTimer.start(); + } + } else { + showTimer.stop(); + hideTimer.stop(); + tooltipVisible = false; + } + } + } + + contentItem: StyledRect { + id: tooltipRect + + implicitWidth: tooltipText.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: tooltipText.implicitHeight + Appearance.padding.smaller * 2 + + color: Colours.palette.m3surfaceContainerHighest + radius: Appearance.rounding.small + + // Add elevation for depth + Elevation { + anchors.fill: parent + radius: parent.radius + z: -1 + level: 3 + } + + StyledText { + id: tooltipText + + anchors.centerIn: parent + + text: root.text + color: Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.small + } + } + + Component.onCompleted: { + if (tooltipVisible) { + updatePosition(); + } + } +} + diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 6877801..a987e75 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -24,8 +24,23 @@ SplitPaneWithDetails { } leftContent: Component { - DeviceList { - session: root.session + StyledFlickable { + id: leftFlickable + + flickableDirection: Flickable.VerticalFlick + contentHeight: deviceList.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } + + DeviceList { + id: deviceList + + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } } } diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 5496966..b260458 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -20,404 +20,427 @@ StyledFlickable { readonly property BluetoothDevice device: session.bt.active flickableDirection: Flickable.VerticalFlick - contentHeight: layoutWrapper.height + contentHeight: detailsWrapper.height StyledScrollBar.vertical: StyledScrollBar { flickable: root } - Item { - id: layoutWrapper + Item { + id: detailsWrapper + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + implicitHeight: details.implicitHeight + + DeviceDetails { + id: details anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - implicitHeight: layout.height - - ColumnLayout { - id: layout - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: Appearance.spacing.normal - - SettingsHeader { - icon: Icons.getBluetoothIcon(root.device?.icon ?? "") - title: root.device?.name ?? "" - } - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Connection status") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - StyledText { - text: qsTr("Connection settings for this device") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: deviceStatus - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + session: root.session + device: root.device - spacing: Appearance.spacing.larger - - Toggle { - label: qsTr("Connected") - checked: root.device?.connected ?? false - toggle.onToggled: root.device.connected = checked - } - - Toggle { - label: qsTr("Paired") - checked: root.device?.paired ?? false - toggle.onToggled: { - if (root.device.paired) - root.device.forget(); - else - root.device.pair(); - } - } - - Toggle { - label: qsTr("Blocked") - checked: root.device?.blocked ?? false - toggle.onToggled: root.device.blocked = checked - } + headerComponent: Component { + SettingsHeader { + icon: Icons.getBluetoothIcon(root.device?.icon ?? "") + title: root.device?.name ?? "" } } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Device properties") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - StyledText { - text: qsTr("Additional settings") - color: Colours.palette.m3outline - } - - StyledRect { - Layout.fillWidth: true - implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: deviceProps + sections: [ + Component { + ColumnLayout { + spacing: Appearance.spacing.normal - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Connection status") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } - spacing: Appearance.spacing.larger + StyledText { + text: qsTr("Connection settings for this device") + color: Colours.palette.m3outline + } - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2 - Item { - id: renameDevice + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - Layout.fillWidth: true - Layout.rightMargin: Appearance.spacing.small + ColumnLayout { + id: deviceStatus - implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large - states: State { - name: "editingDeviceName" - when: root.session.bt.editingDeviceName + spacing: Appearance.spacing.larger - AnchorChanges { - target: deviceNameEdit - anchors.top: renameDevice.top - } - PropertyChanges { - renameDevice.implicitHeight: deviceNameEdit.implicitHeight - renameLabel.opacity: 0 - deviceNameEdit.padding: Appearance.padding.normal + Toggle { + label: qsTr("Connected") + checked: root.device?.connected ?? false + toggle.onToggled: root.device.connected = checked } - } - transitions: Transition { - AnchorAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + Toggle { + label: qsTr("Paired") + checked: root.device?.paired ?? false + toggle.onToggled: { + if (root.device.paired) + root.device.forget(); + else + root.device.pair(); + } } - Anim { - properties: "implicitHeight,opacity,padding" + + Toggle { + label: qsTr("Blocked") + checked: root.device?.blocked ?? false + toggle.onToggled: root.device.blocked = checked } } + } + } + }, + Component { + ColumnLayout { + spacing: Appearance.spacing.normal + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Device properties") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } - StyledText { - id: renameLabel + StyledText { + text: qsTr("Additional settings") + color: Colours.palette.m3outline + } - anchors.left: parent.left + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 - text: qsTr("Device name") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - StyledTextField { - id: deviceNameEdit + ColumnLayout { + id: deviceProps anchors.left: parent.left anchors.right: parent.right - anchors.top: renameLabel.bottom - anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal - - text: root.device?.name ?? "" - readOnly: !root.session.bt.editingDeviceName - onAccepted: { - root.session.bt.editingDeviceName = false; - root.device.name = text; - } - - leftPadding: Appearance.padding.normal - rightPadding: Appearance.padding.normal - - background: StyledRect { - radius: Appearance.rounding.small - border.width: 2 - border.color: Colours.palette.m3primary - opacity: root.session.bt.editingDeviceName ? 1 : 0 + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.larger + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + Item { + id: renameDevice + + Layout.fillWidth: true + Layout.rightMargin: Appearance.spacing.small + + implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight + + states: State { + name: "editingDeviceName" + when: root.session.bt.editingDeviceName + + AnchorChanges { + target: deviceNameEdit + anchors.top: renameDevice.top + } + PropertyChanges { + renameDevice.implicitHeight: deviceNameEdit.implicitHeight + renameLabel.opacity: 0 + deviceNameEdit.padding: Appearance.padding.normal + } + } + + transitions: Transition { + AnchorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + Anim { + properties: "implicitHeight,opacity,padding" + } + } + + StyledText { + id: renameLabel + + anchors.left: parent.left + + text: qsTr("Device name") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledTextField { + id: deviceNameEdit + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: renameLabel.bottom + anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal + + text: root.device?.name ?? "" + readOnly: !root.session.bt.editingDeviceName + onAccepted: { + root.session.bt.editingDeviceName = false; + root.device.name = text; + } + + leftPadding: Appearance.padding.normal + rightPadding: Appearance.padding.normal + + background: StyledRect { + radius: Appearance.rounding.small + border.width: 2 + border.color: Colours.palette.m3primary + opacity: root.session.bt.editingDeviceName ? 1 : 0 + + Behavior on border.color { + CAnim {} + } + + Behavior on opacity { + Anim {} + } + } + + Behavior on anchors.leftMargin { + Anim {} + } + } + } - Behavior on border.color { - CAnim {} + StyledRect { + implicitWidth: implicitHeight + implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.small + color: Colours.palette.m3secondaryContainer + opacity: root.session.bt.editingDeviceName ? 1 : 0 + scale: root.session.bt.editingDeviceName ? 1 : 0.5 + + StateLayer { + color: Colours.palette.m3onSecondaryContainer + disabled: !root.session.bt.editingDeviceName + + function onClicked(): void { + root.session.bt.editingDeviceName = false; + deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); + } + } + + MaterialIcon { + id: cancelEditIcon + + anchors.centerIn: parent + animate: true + text: "cancel" + color: Colours.palette.m3onSecondaryContainer + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } } - Behavior on opacity { - Anim {} + StyledRect { + implicitWidth: implicitHeight + implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) + + StateLayer { + color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + + function onClicked(): void { + root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; + if (root.session.bt.editingDeviceName) + deviceNameEdit.forceActiveFocus(); + else + deviceNameEdit.accepted(); + } + } + + MaterialIcon { + id: editIcon + + anchors.centerIn: parent + animate: true + text: root.session.bt.editingDeviceName ? "check_circle" : "edit" + color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + } + + Behavior on radius { + Anim {} + } } } - Behavior on anchors.leftMargin { - Anim {} + Toggle { + label: qsTr("Trusted") + checked: root.device?.trusted ?? false + toggle.onToggled: root.device.trusted = checked } - } - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 - - radius: Appearance.rounding.small - color: Colours.palette.m3secondaryContainer - opacity: root.session.bt.editingDeviceName ? 1 : 0 - scale: root.session.bt.editingDeviceName ? 1 : 0.5 - - StateLayer { - color: Colours.palette.m3onSecondaryContainer - disabled: !root.session.bt.editingDeviceName - function onClicked(): void { - root.session.bt.editingDeviceName = false; - deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); + Toggle { + label: qsTr("Wake allowed") + checked: root.device?.wakeAllowed ?? false + toggle.onToggled: root.device.wakeAllowed = checked } } + } + } + }, + Component { + ColumnLayout { + spacing: Appearance.spacing.normal + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Device information") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } - MaterialIcon { - id: cancelEditIcon - - anchors.centerIn: parent - animate: true - text: "cancel" - color: Colours.palette.m3onSecondaryContainer - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } + StyledText { + text: qsTr("Information about this device") + color: Colours.palette.m3outline } StyledRect { - implicitWidth: implicitHeight - implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 - - radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) - color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) - - StateLayer { - color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - - function onClicked(): void { - root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; - if (root.session.bt.editingDeviceName) - deviceNameEdit.forceActiveFocus(); - else - deviceNameEdit.accepted(); - } - } - - MaterialIcon { - id: editIcon - - anchors.centerIn: parent - animate: true - text: root.session.bt.editingDeviceName ? "check_circle" : "edit" - color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - } - - Behavior on radius { - Anim {} - } - } - } + Layout.fillWidth: true + implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2 - Toggle { - label: qsTr("Trusted") - checked: root.device?.trusted ?? false - toggle.onToggled: root.device.trusted = checked - } + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - Toggle { - label: qsTr("Wake allowed") - checked: root.device?.wakeAllowed ?? false - toggle.onToggled: root.device.wakeAllowed = checked - } - } - } + ColumnLayout { + id: deviceInfo - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Device information") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large - StyledText { - text: qsTr("Information about this device") - color: Colours.palette.m3outline - } + spacing: Appearance.spacing.small / 2 - StyledRect { - Layout.fillWidth: true - implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2 + StyledText { + text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") + } - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + RowLayout { + Layout.topMargin: Appearance.spacing.small / 2 + Layout.fillWidth: true + Layout.preferredHeight: Appearance.padding.smaller + spacing: Appearance.spacing.small / 2 + + StyledRect { + Layout.fillHeight: true + implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0 + radius: Appearance.rounding.full + color: Colours.palette.m3primary + } - ColumnLayout { - id: deviceInfo + StyledRect { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Appearance.rounding.full + color: Colours.palette.m3secondaryContainer + + StyledRect { + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: parent.height * 0.25 + + implicitWidth: height + radius: Appearance.rounding.full + color: Colours.palette.m3primary + } + } + } - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Dbus path") + } - spacing: Appearance.spacing.small / 2 + StyledText { + text: root.device?.dbusPath ?? "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } - StyledText { - text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") - } + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("MAC address") + } - RowLayout { - Layout.topMargin: Appearance.spacing.small / 2 - Layout.fillWidth: true - Layout.preferredHeight: Appearance.padding.smaller - spacing: Appearance.spacing.small / 2 + StyledText { + text: root.device?.address ?? "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } - StyledRect { - Layout.fillHeight: true - implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0 - radius: Appearance.rounding.full - color: Colours.palette.m3primary - } + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Bonded") + } - StyledRect { - Layout.fillWidth: true - Layout.fillHeight: true - radius: Appearance.rounding.full - color: Colours.palette.m3secondaryContainer + StyledText { + text: root.device?.bonded ? qsTr("Yes") : qsTr("No") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } - StyledRect { - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: parent.height * 0.25 + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("System name") + } - implicitWidth: height - radius: Appearance.rounding.full - color: Colours.palette.m3primary + StyledText { + text: root.device?.deviceName ?? "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } } } } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Dbus path") - } - - StyledText { - text: root.device?.dbusPath ?? "" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("MAC address") - } - - StyledText { - text: root.device?.address ?? "" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Bonded") - } - - StyledText { - text: root.device?.bonded ? qsTr("Yes") : qsTr("No") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("System name") - } - - StyledText { - text: root.device?.deviceName ?? "" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } } - } - } + ] } + } + // FAB Menu (positioned absolutely relative to flickable) ColumnLayout { anchors.right: fabRoot.right anchors.bottom: fabRoot.top diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index b3db236..b978a2d 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -48,8 +48,12 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.enabled ?? false icon: "power" accent: "Tertiary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Toggle Bluetooth") - function onClicked(): void { + onClicked: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.enabled = !adapter.enabled; @@ -60,8 +64,12 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.discoverable ?? false icon: root.smallDiscoverable ? "group_search" : "" label: root.smallDiscoverable ? "" : qsTr("Discoverable") + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Make discoverable") - function onClicked(): void { + onClicked: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.discoverable = !adapter.discoverable; @@ -72,20 +80,44 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.pairable ?? false icon: "missing_controller" label: root.smallPairable ? "" : qsTr("Pairable") + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Make pairable") - function onClicked(): void { + onClicked: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.pairable = !adapter.pairable; } } + ToggleButton { + toggled: Bluetooth.defaultAdapter?.discovering ?? false + icon: "bluetooth_searching" + accent: "Secondary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Scan for devices") + + onClicked: { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.discovering = !adapter.discovering; + } + } + ToggleButton { toggled: !root.session.bt.active icon: "settings" accent: "Primary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Bluetooth settings") - function onClicked(): void { + onClicked: { if (root.session.bt.active) root.session.bt.active = null; else { @@ -96,47 +128,6 @@ DeviceList { } } - titleSuffix: Component { - RowLayout { - spacing: Appearance.spacing.normal - - Item { - Layout.fillWidth: true - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: scanIcon.implicitHeight + Appearance.padding.normal * 2 - - radius: Bluetooth.defaultAdapter?.discovering ? Appearance.rounding.normal : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) - color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - - StateLayer { - color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - - function onClicked(): void { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.discovering = !adapter.discovering; - } - } - - MaterialIcon { - id: scanIcon - - anchors.centerIn: parent - animate: true - text: "bluetooth_searching" - color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - fill: Bluetooth.defaultAdapter?.discovering ? 1 : 0 - } - - Behavior on radius { - Anim {} - } - } - } - } delegate: Component { StyledRect { @@ -146,8 +137,7 @@ DeviceList { readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting) readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected - anchors.left: parent.left - anchors.right: parent.right + width: ListView.view ? ListView.view.width : undefined implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index 256e689..eef0aae 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -90,6 +90,8 @@ Item { model: root.sections Loader { + required property Component modelData + Layout.fillWidth: true sourceComponent: modelData } diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index f8473ff..bf7126f 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -6,6 +6,7 @@ import qs.components.controls import qs.components.containers import qs.services import qs.config +import Quickshell import QtQuick import QtQuick.Layouts @@ -55,6 +56,7 @@ ColumnLayout { property var activeItem: null property Component headerComponent: null property Component titleSuffix: null + property bool showHeader: true signal itemSelected(var item) @@ -66,7 +68,7 @@ ColumnLayout { Layout.fillWidth: true sourceComponent: root.headerComponent - visible: root.headerComponent !== null + visible: root.headerComponent !== null && root.showHeader } // Title and description row @@ -109,17 +111,15 @@ ColumnLayout { id: view Layout.fillWidth: true - Layout.fillHeight: true + // Use contentHeight to show all items without estimation + implicitHeight: contentHeight model: root.model delegate: root.delegate spacing: Appearance.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: view - } + interactive: false // Disable individual scrolling - parent pane handles it + clip: false // Don't clip - let parent handle scrolling } } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index cf965e8..803d7e0 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -183,6 +183,10 @@ Item { toggled: !root.session.launcher.active icon: "settings" accent: "Primary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Launcher settings") onClicked: { if (root.session.launcher.active) { diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index 7c2534a..ad078ec 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects @@ -10,99 +11,108 @@ import qs.config import QtQuick import QtQuick.Layouts -Item { +DeviceDetails { id: root required property Session session - readonly property var device: session.ethernet.active + readonly property var ethernetDevice: session.ethernet.active - implicitWidth: layout.implicitWidth - implicitHeight: layout.implicitHeight + device: ethernetDevice Component.onCompleted: { - if (device && device.interface) { - Nmcli.getEthernetDeviceDetails(device.interface, () => {}); + if (ethernetDevice && ethernetDevice.interface) { + Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {}); } } - onDeviceChanged: { - if (device && device.interface) { - Nmcli.getEthernetDeviceDetails(device.interface, () => {}); + onEthernetDeviceChanged: { + if (ethernetDevice && ethernetDevice.interface) { + Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {}); } else { Nmcli.ethernetDeviceDetails = null; } } - ColumnLayout { - id: layout - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: Appearance.spacing.normal + headerComponent: Component { + ConnectionHeader { + icon: "cable" + title: root.ethernetDevice?.interface ?? qsTr("Unknown") + } + } - ConnectionHeader { - icon: "cable" - title: root.device?.interface ?? qsTr("Unknown") - } + sections: [ + Component { + ColumnLayout { + spacing: Appearance.spacing.normal - SectionHeader { - title: qsTr("Connection status") - description: qsTr("Connection settings for this device") - } + SectionHeader { + title: qsTr("Connection status") + description: qsTr("Connection settings for this device") + } - SectionContainer { - ToggleRow { - label: qsTr("Connected") - checked: root.device?.connected ?? false - toggle.onToggled: { - if (checked) { - Nmcli.connectEthernet(root.device?.connection || "", root.device?.interface || "", () => {}); - } else { - if (root.device?.connection) { - Nmcli.disconnectEthernet(root.device.connection, () => {}); + SectionContainer { + ToggleRow { + label: qsTr("Connected") + checked: root.ethernetDevice?.connected ?? false + toggle.onToggled: { + if (checked) { + Nmcli.connectEthernet(root.ethernetDevice?.connection || "", root.ethernetDevice?.interface || "", () => {}); + } else { + if (root.ethernetDevice?.connection) { + Nmcli.disconnectEthernet(root.ethernetDevice.connection, () => {}); + } } } } } } + }, + Component { + ColumnLayout { + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Device properties") + description: qsTr("Additional information") + } - SectionHeader { - title: qsTr("Device properties") - description: qsTr("Additional information") - } - - SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 - PropertyRow { - label: qsTr("Interface") - value: root.device?.interface ?? qsTr("Unknown") - } + PropertyRow { + label: qsTr("Interface") + value: root.ethernetDevice?.interface ?? qsTr("Unknown") + } - PropertyRow { - showTopMargin: true - label: qsTr("Connection") - value: root.device?.connection || qsTr("Not connected") - } + PropertyRow { + showTopMargin: true + label: qsTr("Connection") + value: root.ethernetDevice?.connection || qsTr("Not connected") + } - PropertyRow { - showTopMargin: true - label: qsTr("State") - value: root.device?.state ?? qsTr("Unknown") + PropertyRow { + showTopMargin: true + label: qsTr("State") + value: root.ethernetDevice?.state ?? qsTr("Unknown") + } } } + }, + Component { + ColumnLayout { + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Connection information") + description: qsTr("Network connection details") + } - SectionHeader { - title: qsTr("Connection information") - description: qsTr("Network connection details") - } - - SectionContainer { - ConnectionInfoSection { - deviceDetails: Nmcli.ethernetDeviceDetails + SectionContainer { + ConnectionInfoSection { + deviceDetails: Nmcli.ethernetDeviceDetails + } } } - } - -} \ No newline at end of file + } + ] +} diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index 03bd37e..ea3ece5 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -39,6 +39,9 @@ DeviceList { toggled: !root.session.ethernet.active icon: "settings" accent: "Primary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller onClicked: { if (root.session.ethernet.active) @@ -55,8 +58,7 @@ DeviceList { StyledRect { required property var modelData - anchors.left: parent.left - anchors.right: parent.right + width: ListView.view ? ListView.view.width : undefined color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index d76e8f5..e28d35c 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -28,23 +28,22 @@ Item { anchors.fill: parent leftContent: Component { + StyledFlickable { + id: leftFlickable - StyledFlickable { - id: leftFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContent.height - flickableDirection: Flickable.VerticalFlick - contentHeight: leftContent.height - - StyledScrollBar.vertical: StyledScrollBar { - flickable: leftFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } - ColumnLayout { - id: leftContent + ColumnLayout { + id: leftContent - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.normal + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal // Network header above the collapsible sections RowLayout { @@ -65,6 +64,10 @@ Item { toggled: Nmcli.wifiEnabled icon: "wifi" accent: "Tertiary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Toggle WiFi") onClicked: { Nmcli.toggleWifi(null); @@ -75,6 +78,10 @@ Item { toggled: Nmcli.scanning icon: "wifi_find" accent: "Secondary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Scan for networks") onClicked: { Nmcli.rescanWifi(); @@ -85,6 +92,10 @@ Item { toggled: !root.session.ethernet.active && !root.session.network.active icon: "settings" accent: "Primary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Network settings") onClicked: { if (root.session.ethernet.active || root.session.network.active) { @@ -109,127 +120,12 @@ Item { title: qsTr("Ethernet") expanded: true - ColumnLayout { + Loader { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - } - - StyledText { - Layout.fillWidth: true - text: qsTr("All available ethernet devices") - color: Colours.palette.m3outline - } - - Repeater { - id: ethernetRepeater - - Layout.fillWidth: true - model: Nmcli.ethernetDevices - - delegate: StyledRect { - required property var modelData - - Layout.fillWidth: true - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - - StateLayer { - function onClicked(): void { - root.session.network.active = null; - root.session.ethernet.active = modelData; - } - } - - RowLayout { - id: rowLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 - - radius: Appearance.rounding.normal - color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - - MaterialIcon { - id: icon - - anchors.centerIn: parent - text: "cable" - font.pointSize: Appearance.font.size.large - fill: modelData.connected ? 1 : 0 - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - } - - ColumnLayout { - Layout.fillWidth: true - - spacing: 0 - - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - - text: modelData.interface || qsTr("Unknown") - } - - StyledText { - Layout.fillWidth: true - text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") - color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: modelData.connected ? 500 : 400 - elide: Text.ElideRight - } - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 - - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) - - StateLayer { - function onClicked(): void { - if (modelData.connected && modelData.connection) { - Nmcli.disconnectEthernet(modelData.connection, () => {}); - } else { - Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); - } - } - } - - MaterialIcon { - id: connectIcon - - anchors.centerIn: parent - text: modelData.connected ? "link_off" : "link" - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - } - } - - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + sourceComponent: Component { + EthernetList { + session: root.session + showHeader: false } } } @@ -242,195 +138,12 @@ Item { title: qsTr("Wireless") expanded: true - ColumnLayout { + Loader { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - text: qsTr("Networks (%1)").arg(Nmcli.networks.length) - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - - StyledText { - visible: Nmcli.scanning - text: qsTr("Scanning...") - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.small - } - } - - StyledText { - Layout.fillWidth: true - text: qsTr("All available WiFi networks") - color: Colours.palette.m3outline - } - - Repeater { - id: wirelessRepeater - - Layout.fillWidth: true - model: ScriptModel { - values: [...Nmcli.networks].sort((a, b) => { - // Put active/connected network first - if (a.active !== b.active) - return b.active - a.active; - // Then sort by signal strength - return b.strength - a.strength; - }) - } - - delegate: StyledRect { - required property var modelData - - Layout.fillWidth: true - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (modelData && root.session.network.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - - StateLayer { - function onClicked(): void { - if (!modelData) { - return; - } - root.session.ethernet.active = null; - root.session.network.active = modelData; - // Check if we need to refresh saved connections when selecting a network - if (modelData.ssid) { - checkSavedProfileForNetwork(modelData.ssid); - } - } - } - - RowLayout { - id: wirelessRowLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: wirelessIcon.implicitHeight + Appearance.padding.normal * 2 - - radius: Appearance.rounding.normal - color: (modelData && modelData.active) ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - - MaterialIcon { - id: wirelessIcon - - anchors.centerIn: parent - text: Icons.getNetworkIcon(modelData && modelData.strength !== undefined ? modelData.strength : 0) - font.pointSize: Appearance.font.size.large - fill: (modelData && modelData.active) ? 1 : 0 - color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - - StyledRect { - id: lockBadge - - visible: modelData && modelData.isSecure - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: -Appearance.padding.smaller / 2 - - implicitWidth: lockIconSize + Appearance.padding.smaller - implicitHeight: lockIconSize + Appearance.padding.smaller - radius: Appearance.rounding.full - color: Colours.palette.m3secondaryContainer - - readonly property real lockIconSize: lockIcon.implicitWidth - - Elevation { - anchors.fill: parent - radius: parent.radius - z: -1 - level: 2 - } - - MaterialIcon { - id: lockIcon - - anchors.centerIn: parent - text: "lock" - font.pointSize: Appearance.font.size.small - fill: 1 - color: Colours.palette.m3onSurface - } - } - } - - ColumnLayout { - Layout.fillWidth: true - - spacing: 0 - - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - - text: (modelData && modelData.ssid) ? modelData.ssid : qsTr("Unknown") - } - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.smaller - - StyledText { - Layout.fillWidth: true - text: { - if (!modelData) return qsTr("Open"); - if (modelData.active) return qsTr("Connected"); - if (modelData.isSecure && modelData.security && modelData.security.length > 0) { - return modelData.security; - } - if (modelData.isSecure) return qsTr("Secured"); - return qsTr("Open"); - } - color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: (modelData && modelData.active) ? 500 : 400 - elide: Text.ElideRight - } - } - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2 - - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, (modelData && modelData.active) ? 1 : 0) - - StateLayer { - function onClicked(): void { - if (modelData && modelData.active) { - Nmcli.disconnectFromNetwork(); - } else if (modelData) { - NetworkConnection.handleConnect(modelData, root.session, null); - } - } - } - - MaterialIcon { - id: wirelessConnectIcon - - anchors.centerIn: parent - text: (modelData && modelData.active) ? "link_off" : "link" - color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - } - } - - implicitHeight: wirelessRowLayout.implicitHeight + Appearance.padding.normal * 2 + sourceComponent: Component { + WirelessList { + session: root.session + showHeader: false } } } @@ -443,16 +156,17 @@ Item { 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 ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings") - property Component targetComponent: settings - property Component nextComponent: settings + property Component targetComponent: settingsComponent + property Component nextComponent: settingsComponent function getComponentForPane() { - return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings; + if (ethernetPane) return ethernetDetailsComponent; + if (wirelessPane) return wirelessDetailsComponent; + return settingsComponent; } Component.onCompleted: { @@ -507,7 +221,7 @@ Item { } Component { - id: settings + id: settingsComponent StyledFlickable { id: settingsFlickable @@ -530,7 +244,7 @@ Item { } Component { - id: ethernetDetails + id: ethernetDetailsComponent StyledFlickable { id: ethernetFlickable @@ -553,7 +267,7 @@ Item { } Component { - id: wirelessDetails + id: wirelessDetailsComponent StyledFlickable { id: wirelessFlickable @@ -580,11 +294,4 @@ Item { session: root.session z: 1000 } - - function checkSavedProfileForNetwork(ssid: string): void { - if (ssid && ssid.length > 0) { - Nmcli.loadSavedConnections(() => {}); - } - } } - diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 57c06c8..7f6a4aa 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import "." import qs.components import qs.components.controls @@ -12,14 +13,13 @@ import qs.utils import QtQuick import QtQuick.Layouts -Item { +DeviceDetails { id: root required property Session session readonly property var network: session.network.active - implicitWidth: layout.implicitWidth - implicitHeight: layout.implicitHeight + device: network Component.onCompleted: { updateDeviceDetails(); @@ -102,110 +102,120 @@ Item { } } - ColumnLayout { - id: layout - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: Appearance.spacing.normal - - ConnectionHeader { - icon: root.network?.isSecure ? "lock" : "wifi" - title: root.network?.ssid ?? qsTr("Unknown") - } + headerComponent: Component { + ConnectionHeader { + icon: root.network?.isSecure ? "lock" : "wifi" + title: root.network?.ssid ?? qsTr("Unknown") + } + } - SectionHeader { - title: qsTr("Connection status") - description: qsTr("Connection settings for this network") - } + sections: [ + Component { + ColumnLayout { + spacing: Appearance.spacing.normal - SectionContainer { - ToggleRow { - label: qsTr("Connected") - checked: root.network?.active ?? false - toggle.onToggled: { - if (checked) { - NetworkConnection.handleConnect(root.network, root.session, null); - } else { - Nmcli.disconnectFromNetwork(); - } - } + SectionHeader { + title: qsTr("Connection status") + description: qsTr("Connection settings for this network") } - TextButton { - Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 - visible: { - if (!root.network || !root.network.ssid) { - return false; + SectionContainer { + ToggleRow { + label: qsTr("Connected") + checked: root.network?.active ?? false + toggle.onToggled: { + if (checked) { + NetworkConnection.handleConnect(root.network, root.session, null); + } else { + Nmcli.disconnectFromNetwork(); + } } - return Nmcli.hasSavedProfile(root.network.ssid); } - inactiveColour: Colours.palette.m3secondaryContainer - inactiveOnColour: Colours.palette.m3onSecondaryContainer - text: qsTr("Forget Network") - onClicked: { - if (root.network && root.network.ssid) { - if (root.network.active) { - Nmcli.disconnectFromNetwork(); + TextButton { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + visible: { + if (!root.network || !root.network.ssid) { + return false; + } + return Nmcli.hasSavedProfile(root.network.ssid); + } + inactiveColour: Colours.palette.m3secondaryContainer + inactiveOnColour: Colours.palette.m3onSecondaryContainer + text: qsTr("Forget Network") + + onClicked: { + if (root.network && root.network.ssid) { + if (root.network.active) { + Nmcli.disconnectFromNetwork(); + } + Nmcli.forgetNetwork(root.network.ssid); } - Nmcli.forgetNetwork(root.network.ssid); } } } } + }, + Component { + ColumnLayout { + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Network properties") + description: qsTr("Additional information") + } - SectionHeader { - title: qsTr("Network properties") - description: qsTr("Additional information") - } - - SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 - PropertyRow { - label: qsTr("SSID") - value: root.network?.ssid ?? qsTr("Unknown") - } + PropertyRow { + label: qsTr("SSID") + value: root.network?.ssid ?? qsTr("Unknown") + } - PropertyRow { - showTopMargin: true - label: qsTr("BSSID") - value: root.network?.bssid ?? qsTr("Unknown") - } + PropertyRow { + showTopMargin: true + label: qsTr("BSSID") + value: root.network?.bssid ?? qsTr("Unknown") + } - PropertyRow { - showTopMargin: true - label: qsTr("Signal strength") - value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A") - } + PropertyRow { + showTopMargin: true + label: qsTr("Signal strength") + value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A") + } - PropertyRow { - showTopMargin: true - label: qsTr("Frequency") - value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A") - } + PropertyRow { + showTopMargin: true + label: qsTr("Frequency") + value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A") + } - PropertyRow { - showTopMargin: true - label: qsTr("Security") - value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A") + PropertyRow { + showTopMargin: true + label: qsTr("Security") + value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A") + } } } + }, + Component { + ColumnLayout { + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Connection information") + description: qsTr("Network connection details") + } - SectionHeader { - title: qsTr("Connection information") - description: qsTr("Network connection details") - } - - SectionContainer { - ConnectionInfoSection { - deviceDetails: Nmcli.wirelessDeviceDetails + SectionContainer { + ConnectionInfoSection { + deviceDetails: Nmcli.wirelessDeviceDetails + } } } - } - + } + ] } diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 2f0288f..4726712 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -9,6 +9,7 @@ import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell import QtQuick import QtQuick.Layouts @@ -58,6 +59,9 @@ DeviceList { toggled: Nmcli.wifiEnabled icon: "wifi" accent: "Tertiary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller onClicked: { Nmcli.toggleWifi(null); @@ -68,6 +72,9 @@ DeviceList { toggled: Nmcli.scanning icon: "wifi_find" accent: "Secondary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller onClicked: { Nmcli.rescanWifi(); @@ -78,6 +85,9 @@ DeviceList { toggled: !root.session.network.active icon: "settings" accent: "Primary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller onClicked: { if (root.session.network.active) @@ -94,8 +104,7 @@ DeviceList { StyledRect { required property var modelData - anchors.left: parent.left - anchors.right: parent.right + width: ListView.view ? ListView.view.width : undefined color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal -- cgit v1.2.3-freya From 978a3f430226ae6cead3284329b4dcb309e979ee Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 21:07:19 -0500 Subject: cleanup: removed comments --- modules/controlcenter/PaneRegistry.qml | 46 ---------------------- modules/controlcenter/components/DeviceDetails.qml | 32 --------------- modules/controlcenter/components/DeviceList.qml | 34 ---------------- .../controlcenter/components/SettingsHeader.qml | 21 ---------- .../components/SplitPaneWithDetails.qml | 35 ---------------- 5 files changed, 168 deletions(-) diff --git a/modules/controlcenter/PaneRegistry.qml b/modules/controlcenter/PaneRegistry.qml index df63c6e..d8bf45e 100644 --- a/modules/controlcenter/PaneRegistry.qml +++ b/modules/controlcenter/PaneRegistry.qml @@ -2,28 +2,9 @@ pragma Singleton import QtQuick -/** - * PaneRegistry - * - * Centralized registry for Control Center panes. This singleton provides a single - * source of truth for pane metadata (id, label, icon, component), eliminating - * the need for manual index management and making adding/removing panes trivial. - * - * Usage: - * - Panes.qml: Dynamically creates panes from registry - * - Session.qml: Derives panes list from registry - * - NavRail.qml: Uses registry for navigation items - */ QtObject { id: root - /** - * Pane metadata structure: - * - id: Unique identifier for the pane (string) - * - label: Display label for the pane (string) - * - icon: Material icon name (string) - * - component: Component path relative to controlcenter module (string) - */ readonly property list panes: [ QtObject { readonly property string id: "network" @@ -63,15 +44,8 @@ QtObject { } ] - /** - * Get the count of registered panes - */ readonly property int count: panes.length - /** - * Get pane labels as a list of strings - * Useful for Session.qml's panes property - */ readonly property var labels: { const result = []; for (let i = 0; i < panes.length; i++) { @@ -80,11 +54,6 @@ QtObject { return result; } - /** - * Get pane metadata by index - * @param index The index of the pane - * @return The pane metadata object or null if index is out of bounds - */ function getByIndex(index: int): QtObject { if (index >= 0 && index < panes.length) { return panes[index]; @@ -92,11 +61,6 @@ QtObject { return null; } - /** - * Get pane index by label - * @param label The label to search for - * @return The index of the pane or -1 if not found - */ function getIndexByLabel(label: string): int { for (let i = 0; i < panes.length; i++) { if (panes[i].label === label) { @@ -106,21 +70,11 @@ QtObject { return -1; } - /** - * Get pane metadata by label - * @param label The label to search for - * @return The pane metadata object or null if not found - */ function getByLabel(label: string): QtObject { const index = getIndexByLabel(label); return getByIndex(index); } - /** - * Get pane metadata by id - * @param id The id to search for - * @return The pane metadata object or null if not found - */ function getById(id: string): QtObject { for (let i = 0; i < panes.length; i++) { if (panes[i].id === id) { diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index eef0aae..768e77a 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -9,38 +9,6 @@ import qs.config import QtQuick import QtQuick.Layouts -/** - * DeviceDetails - * - * A reusable base component for displaying device/network details with a standardized - * structure. Provides a header, connection status section, and flexible sections for - * device-specific information. - * - * This component eliminates duplication across WirelessDetails, EthernetDetails, and Bluetooth Details - * by providing a common structure while allowing full customization of sections. - * - * Usage: - * ```qml - * DeviceDetails { - * session: root.session - * device: session.network.active - * headerComponent: Component { - * ConnectionHeader { - * icon: "wifi" - * title: device?.ssid ?? "" - * } - * } - * sections: [ - * Component { - * // Connection status section - * }, - * Component { - * // Properties section - * } - * ] - * } - * ``` - */ Item { id: root diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index bf7126f..a6821d8 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -10,40 +10,6 @@ import Quickshell import QtQuick import QtQuick.Layouts -/** - * DeviceList - * - * A reusable base component for displaying lists of devices/networks with a standardized - * structure. Provides a header with action buttons, title/subtitle, and a scrollable list - * with customizable delegates. - * - * This component eliminates duplication across WirelessList, EthernetList, and Bluetooth DeviceList - * by providing a common structure while allowing full customization of headers and delegates. - * - * Usage: - * ```qml - * DeviceList { - * session: root.session - * title: qsTr("Networks (%1)").arg(Nmcli.networks.length) - * description: qsTr("All available WiFi networks") - * model: ScriptModel { - * values: [...Nmcli.networks].sort(...) - * } - * activeItem: session.network.active - * onItemSelected: (item) => { - * session.network.active = item; - * } - * headerComponent: Component { - * RowLayout { - * // Custom header buttons - * } - * } - * delegate: Component { - * // Custom delegate for each item - * } - * } - * ``` - */ ColumnLayout { id: root diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml index 9a77968..c1ba148 100644 --- a/modules/controlcenter/components/SettingsHeader.qml +++ b/modules/controlcenter/components/SettingsHeader.qml @@ -5,31 +5,10 @@ import qs.config import QtQuick import QtQuick.Layouts -/** - * SettingsHeader - * - * Reusable header component for settings panes. Displays a large icon and title - * in a consistent format across all settings screens. - * - * Usage: - * ```qml - * SettingsHeader { - * icon: "router" - * title: qsTr("Network Settings") - * } - * ``` - */ Item { id: root - /** - * Material icon name to display - */ required property string icon - - /** - * Title text to display - */ required property string title Layout.fillWidth: true diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index e139aca..6af8c1a 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -9,41 +9,6 @@ import Quickshell.Widgets import QtQuick import QtQuick.Layouts -/** - * SplitPaneWithDetails - * - * A reusable component that provides a split-pane layout with a list on the left - * and a details/settings view on the right. The right pane automatically switches - * between details and settings views based on whether an item is selected. - * - * This component eliminates duplication across WirelessPane, EthernetPane, and BtPane - * by providing a standardized pattern for split-pane layouts with transition animations. - * - * Usage: - * ```qml - * SplitPaneWithDetails { - * activeItem: root.session.network.active - * leftContent: Component { - * WirelessList { - * session: root.session - * } - * } - * rightDetailsComponent: Component { - * WirelessDetails { - * session: root.session - * } - * } - * rightSettingsComponent: Component { - * StyledFlickable { - * WirelessSettings { - * session: root.session - * } - * } - * } - * paneIdGenerator: (item) => item ? (item.ssid || item.bssid || "") : "" - * } - * ``` - */ Item { id: root -- cgit v1.2.3-freya From 147410e39bf4e0474deca3980dcaa724464cf5c3 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 21:15:40 -0500 Subject: cleanup: removed unnecessary comments --- modules/controlcenter/ControlCenter.qml | 1 - modules/controlcenter/Panes.qml | 18 ------------ modules/controlcenter/Session.qml | 1 - .../controlcenter/appearance/AppearancePane.qml | 28 ++----------------- modules/controlcenter/audio/AudioPane.qml | 1 - modules/controlcenter/bluetooth/Details.qml | 1 - modules/controlcenter/bluetooth/Settings.qml | 4 +-- modules/controlcenter/components/DeviceDetails.qml | 7 ----- modules/controlcenter/components/DeviceList.qml | 10 ++----- .../controlcenter/components/PaneTransition.qml | 12 -------- .../controlcenter/components/SplitPaneLayout.qml | 8 ------ .../components/SplitPaneWithDetails.qml | 2 -- modules/controlcenter/launcher/LauncherPane.qml | 27 ++---------------- modules/controlcenter/network/NetworkingPane.qml | 2 -- modules/controlcenter/network/WirelessDetails.qml | 9 ------ modules/controlcenter/network/WirelessList.qml | 3 -- .../network/WirelessPasswordDialog.qml | 27 ++---------------- modules/controlcenter/state/BluetoothState.qml | 5 ---- modules/controlcenter/state/EthernetState.qml | 1 - modules/controlcenter/state/LauncherState.qml | 1 - modules/controlcenter/state/NetworkState.qml | 3 -- modules/controlcenter/taskbar/TaskbarPane.qml | 32 ---------------------- 22 files changed, 9 insertions(+), 194 deletions(-) diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index 3642a33..fdb824e 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -97,6 +97,5 @@ Item { } } - // Expose initialOpeningComplete for NavRail to prevent tab switching during opening animation readonly property bool initialOpeningComplete: panes.initialOpeningComplete } diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index b9256a9..833a411 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -19,7 +19,6 @@ ClippingRectangle { required property Session session - // Expose initialOpeningComplete so parent can check if opening animation is done readonly property bool initialOpeningComplete: layout.initialOpeningComplete color: "transparent" @@ -27,7 +26,6 @@ ClippingRectangle { focus: false activeFocusOnTab: false - // Clear focus when clicking anywhere in the panes area MouseArea { anchors.fill: parent z: -1 @@ -37,7 +35,6 @@ ClippingRectangle { } } - // Clear focus when switching panes Connections { target: root.session @@ -54,8 +51,6 @@ ClippingRectangle { clip: true property bool animationComplete: true - // Track if initial opening animation has completed - // During initial opening, only the active pane loads to avoid hiccups property bool initialOpeningComplete: false Timer { @@ -66,8 +61,6 @@ ClippingRectangle { } } - // Timer to detect when initial opening animation completes - // Uses large duration to cover both normal and detached opening cases Timer { id: initialOpeningTimer interval: Appearance.anim.durations.large @@ -94,7 +87,6 @@ ClippingRectangle { Connections { target: root.session function onActiveIndexChanged(): void { - // Mark animation as incomplete and start delay timer layout.animationComplete = false; animationDelayTimer.restart(); } @@ -110,28 +102,21 @@ ClippingRectangle { implicitWidth: root.width implicitHeight: root.height - // Track if this pane has ever been loaded to enable caching property bool hasBeenLoaded: false - // Function to compute if this pane should be active function updateActive(): void { const diff = Math.abs(root.session.activeIndex - pane.paneIndex); const isActivePane = diff === 0; let shouldBeActive = false; - // During initial opening animation, only load the active pane - // This prevents hiccups from multiple panes loading simultaneously if (!layout.initialOpeningComplete) { shouldBeActive = isActivePane; } else { - // After initial opening, allow current and adjacent panes for smooth transitions if (diff <= 1) { shouldBeActive = true; } else if (pane.hasBeenLoaded) { - // For distant panes that have been loaded before, keep them active to preserve cached data shouldBeActive = true; } else { - // For new distant panes, wait until animation completes to avoid heavy loading during transition shouldBeActive = layout.animationComplete; } } @@ -152,12 +137,10 @@ ClippingRectangle { } onActiveChanged: { - // Mark pane as loaded when it becomes active if (active && !pane.hasBeenLoaded) { pane.hasBeenLoaded = true; } - // Load the component with initial properties when activated if (active && !item) { loader.setSource(pane.componentPath, { "session": root.session @@ -166,7 +149,6 @@ ClippingRectangle { } onItemChanged: { - // Mark pane as loaded when item is created if (item) { pane.hasBeenLoaded = true; } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index 9c6a754..0408a1a 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -11,7 +11,6 @@ QtObject { property int activeIndex: 0 property bool navExpanded: false - // Pane-specific state objects readonly property BluetoothState bt: BluetoothState {} readonly property NetworkState network: NetworkState {} readonly property EthernetState ethernet: EthernetState {} diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 3ba0549..5b7e859 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -22,7 +22,6 @@ Item { required property Session session - // Appearance settings property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1 property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded" property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF" @@ -37,7 +36,6 @@ Item { property real borderRounding: Config.border.rounding ?? 1 property real borderThickness: Config.border.thickness ?? 1 - // Background settings property bool desktopClockEnabled: Config.background.desktopClock.enabled ?? false property bool backgroundEnabled: Config.background.enabled ?? true property bool visualiserEnabled: Config.background.visualiser.enabled ?? false @@ -127,13 +125,8 @@ Item { 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); @@ -150,7 +143,6 @@ Item { onActiveChanged: { if (!active && wallpaperLoader.item) { const container = wallpaperLoader.item; - // Access timer through wallpaperGrid if (container && container.wallpaperGrid) { const grid = container.wallpaperGrid; if (grid.imageUpdateTimer) { @@ -186,20 +178,17 @@ Item { } } - // 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 loadedCount: 0 + property int visibleCount: 0 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); @@ -216,7 +205,6 @@ Item { } 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); @@ -237,7 +225,6 @@ Item { 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; @@ -255,7 +242,6 @@ Item { topMargin: 0 bottomMargin: 0 - // Use ListModel for incremental updates to prevent flashing when new items are added ListModel { id: wallpaperListModel } @@ -270,7 +256,6 @@ Item { 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; @@ -279,7 +264,6 @@ Item { wallpaperListModel.append({modelData: lazyModel.sourceList[i]}); } - // Preserve scroll position after model update if (flickable) { Qt.callLater(function() { if (Math.abs(flickable.contentY - oldScrollY) < 1) { @@ -382,7 +366,6 @@ Item { 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); @@ -434,7 +417,6 @@ Item { 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); @@ -450,8 +432,6 @@ Item { } } - - // Parent Flickable handles scrolling interactive: false @@ -786,11 +766,9 @@ Item { function onClicked(): void { const variant = modelData.variant; - // Optimistic update - set immediately for responsive UI Schemes.currentVariant = variant; Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); - // Reload after a delay to confirm changes Qt.callLater(() => { reloadTimer.restart(); }); @@ -873,11 +851,9 @@ Item { const flavour = modelData.flavour; const schemeKey = `${name} ${flavour}`; - // Optimistic update - set immediately for responsive UI Schemes.currentScheme = schemeKey; Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - // Reload after a delay to confirm changes Qt.callLater(() => { reloadTimer.restart(); }); diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 9b0c7d2..694e178 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -40,7 +40,6 @@ Item { anchors.right: parent.right spacing: Appearance.spacing.normal - // Audio header above the collapsible sections RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index b260458..5299045 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -440,7 +440,6 @@ StyledFlickable { } } - // FAB Menu (positioned absolutely relative to flickable) ColumnLayout { anchors.right: fabRoot.right anchors.bottom: fabRoot.top diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index b3245ab..c547240 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -328,7 +328,7 @@ ColumnLayout { anchors.left: parent.left - text: qsTr("Rename adapter (currently does not work)") // FIXME: remove disclaimer when fixed + text: qsTr("Rename adapter (currently does not work)") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } @@ -345,8 +345,6 @@ ColumnLayout { readOnly: !root.session.bt.editingAdapterName onAccepted: { root.session.bt.editingAdapterName = false; - // Doesn't work for now, will be added to QS later - // root.session.bt.currentAdapter.name = text; } leftPadding: Appearance.padding.normal diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index 768e77a..d2e8835 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -18,10 +18,7 @@ Item { property Component headerComponent: null property list sections: [] - // Optional: Custom content to insert after header but before sections property Component topContent: null - - // Optional: Custom content to insert after all sections property Component bottomContent: null implicitWidth: layout.implicitWidth @@ -35,7 +32,6 @@ Item { anchors.top: parent.top spacing: Appearance.spacing.normal - // Header component (e.g., ConnectionHeader or SettingsHeader) Loader { id: headerLoader @@ -44,7 +40,6 @@ Item { visible: root.headerComponent !== null } - // Top content (optional) Loader { id: topContentLoader @@ -53,7 +48,6 @@ Item { visible: root.topContent !== null } - // Sections Repeater { model: root.sections @@ -65,7 +59,6 @@ Item { } } - // Bottom content (optional) Loader { id: bottomContentLoader diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index a6821d8..75dd913 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -28,7 +28,6 @@ ColumnLayout { spacing: Appearance.spacing.small - // Header with action buttons (optional) Loader { id: headerLoader @@ -37,7 +36,6 @@ ColumnLayout { visible: root.headerComponent !== null && root.showHeader } - // Title and description row RowLayout { Layout.fillWidth: true Layout.topMargin: root.headerComponent ? 0 : 0 @@ -61,10 +59,8 @@ ColumnLayout { } } - // Expose view for access from parent components property alias view: view - // Description text StyledText { visible: root.description !== "" Layout.fillWidth: true @@ -72,20 +68,18 @@ ColumnLayout { color: Colours.palette.m3outline } - // List view StyledListView { id: view Layout.fillWidth: true - // Use contentHeight to show all items without estimation implicitHeight: contentHeight model: root.model delegate: root.delegate spacing: Appearance.spacing.small / 2 - interactive: false // Disable individual scrolling - parent pane handles it - clip: false // Don't clip - let parent handle scrolling + interactive: false + clip: false } } diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml index 1da4afb..d1814b5 100644 --- a/modules/controlcenter/components/PaneTransition.qml +++ b/modules/controlcenter/components/PaneTransition.qml @@ -3,26 +3,17 @@ pragma ComponentBehavior: Bound import qs.config import QtQuick -// Reusable pane transition animation component -// Provides standard fade-out/scale-down → update → fade-in/scale-up animation -// Used when switching between detail/settings views in panes SequentialAnimation { id: root - // The Loader element to animate required property Item target - - // Optional list of PropertyActions to execute during the transition - // These typically update the component being displayed property list propertyActions - // Animation parameters (with sensible defaults) property real scaleFrom: 1.0 property real scaleTo: 0.8 property real opacityFrom: 1.0 property real opacityTo: 0.0 - // Fade out and scale down ParallelAnimation { NumberAnimation { target: root.target @@ -45,8 +36,6 @@ SequentialAnimation { } } - // Execute property actions (component switching, state updates, etc.) - // This is where the component change happens while invisible ScriptAction { script: { for (let i = 0; i < root.propertyActions.length; i++) { @@ -58,7 +47,6 @@ SequentialAnimation { } } - // Fade in and scale up ParallelAnimation { NumberAnimation { target: root.target diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index 7bd7db0..8b4f0d9 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -15,19 +15,14 @@ RowLayout { 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 @@ -57,7 +52,6 @@ RowLayout { asynchronous: true sourceComponent: root.leftContent - // Apply any additional properties from leftLoaderProperties Component.onCompleted: { for (const key in root.leftLoaderProperties) { leftLoader[key] = root.leftLoaderProperties[key]; @@ -74,7 +68,6 @@ RowLayout { } } - // Right pane Item { id: rightPane @@ -101,7 +94,6 @@ RowLayout { asynchronous: true sourceComponent: root.rightContent - // Apply any additional properties from rightLoaderProperties Component.onCompleted: { for (const key in root.rightLoaderProperties) { rightLoader[key] = root.rightLoaderProperties[key]; diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index 6af8c1a..e873923 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -19,7 +19,6 @@ Item { property var activeItem: null property var paneIdGenerator: function(item) { return item ? String(item) : ""; } - // Optional: Additional component to overlay on top (e.g., password dialogs) property Component overlayComponent: null SplitPaneLayout { @@ -82,7 +81,6 @@ Item { } } - // Overlay component (e.g., password dialogs) Loader { id: overlayLoader diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 803d7e0..47f87cc 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -62,26 +62,20 @@ Item { const appId = root.selectedApp.id || root.selectedApp.entry?.id; - // Create a new array to ensure change detection const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; if (isHidden) { - // Add to hiddenApps if not already there if (!hiddenApps.includes(appId)) { hiddenApps.push(appId); } } else { - // Remove from hiddenApps const index = hiddenApps.indexOf(appId); if (index !== -1) { hiddenApps.splice(index, 1); } } - // Update Config Config.launcher.hiddenApps = hiddenApps; - - // Persist changes to disk Config.save(); } @@ -90,15 +84,13 @@ Item { id: allAppsDb path: `${Paths.state}/apps.sqlite` - entries: DesktopEntries.applications.values // No filter - show all apps + entries: DesktopEntries.applications.values } property string searchText: "" function filterApps(search: string): list { - // If search is empty, return all apps directly if (!search || search.trim() === "") { - // Convert QQmlListProperty to array const apps = []; for (let i = 0; i < allAppsDb.apps.length; i++) { apps.push(allAppsDb.apps[i]); @@ -110,7 +102,6 @@ Item { return []; } - // Prepare apps for fuzzy search const preparedApps = []; for (let i = 0; i < allAppsDb.apps.length; i++) { const app = allAppsDb.apps[i]; @@ -121,14 +112,12 @@ Item { }); } - // Perform fuzzy search const results = Fuzzy.go(search, preparedApps, { all: true, keys: ["name"], scoreFn: r => r[0].score }); - // Return sorted by score (highest first) return results .sort((a, b) => b._score - a._score) .map(r => r.obj._item); @@ -192,7 +181,6 @@ Item { if (root.session.launcher.active) { root.session.launcher.active = null; } else { - // Toggle to show settings - if there are apps, select the first one, otherwise show settings if (root.filteredApps.length > 0) { root.session.launcher.active = root.filteredApps[0]; } @@ -302,13 +290,7 @@ Item { Layout.fillWidth: true Layout.fillHeight: true asynchronous: true - active: { - // 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 - // Access loader through parent - this will be set when component loads - return true; - } + active: true sourceComponent: StyledListView { id: appsListView @@ -418,11 +400,9 @@ Item { 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; } @@ -508,12 +488,10 @@ Item { id: appDetailsLayout anchors.fill: parent - // Get displayedApp from parent Loader (the Loader has displayedApp property we set) readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null spacing: Appearance.spacing.normal - // Show SettingsHeader when no app is selected, or show app icon + title when app is selected SettingsHeader { Layout.leftMargin: Appearance.padding.large * 2 Layout.rightMargin: Appearance.padding.large * 2 @@ -523,7 +501,6 @@ Item { title: qsTr("Launcher Applications") } - // App icon and title display (shown when app is selected) Item { Layout.alignment: Qt.AlignHCenter Layout.leftMargin: Appearance.padding.large * 2 diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index e28d35c..4446428 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -45,7 +45,6 @@ Item { anchors.right: parent.right spacing: Appearance.spacing.normal - // Network header above the collapsible sections RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller @@ -102,7 +101,6 @@ Item { root.session.ethernet.active = null; root.session.network.active = null; } else { - // Toggle to show settings - prefer ethernet if available, otherwise wireless if (Nmcli.ethernetDevices.length > 0) { root.session.ethernet.active = Nmcli.ethernetDevices[0]; } else if (Nmcli.networks.length > 0) { diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 7f6a4aa..cf16400 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -27,7 +27,6 @@ DeviceDetails { } onNetworkChanged: { - // Restart timer when network changes connectionUpdateTimer.stop(); if (network && network.ssid) { connectionUpdateTimer.start(); @@ -48,11 +47,9 @@ DeviceDetails { updateDeviceDetails(); } function onWirelessDeviceDetailsChanged() { - // When details are updated, check if we should stop the timer if (network && network.ssid) { const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { - // We have details for the active network, stop the timer connectionUpdateTimer.stop(); } } @@ -65,22 +62,16 @@ DeviceDetails { repeat: true running: network && network.ssid onTriggered: { - // Periodically check if network becomes active and update details if (network) { const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); if (isActive) { - // Network is active - check if we have details if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { - // Network is active but we don't have details yet, fetch them Nmcli.getWirelessDeviceDetails("", () => { - // After fetching, check if we got details - if not, timer will try again }); } else { - // We have details, can stop the timer connectionUpdateTimer.stop(); } } else { - // Network is not active, clear details if (Nmcli.wirelessDeviceDetails !== null) { Nmcli.wirelessDeviceDetails = null; } diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 4726712..9dabe9d 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -33,10 +33,8 @@ DeviceList { model: ScriptModel { values: [...Nmcli.networks].sort((a, b) => { - // Put active/connected network first if (a.active !== b.active) return b.active - a.active; - // Then sort by signal strength return b.strength - a.strength; }) } @@ -114,7 +112,6 @@ DeviceList { StateLayer { function onClicked(): void { root.session.network.active = modelData; - // Check if we need to refresh saved connections when selecting a network if (modelData && modelData.ssid) { root.checkSavedProfileForNetwork(modelData.ssid); } diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 0f1a5cd..7c046af 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -19,7 +19,6 @@ Item { required property Session session readonly property var network: { - // Prefer pendingNetwork, then active network if (session.network.pendingNetwork) { return session.network.pendingNetwork; } @@ -157,12 +156,10 @@ Item { focus: true Keys.onPressed: event => { - // Ensure we have focus when receiving keyboard input if (!activeFocus) { forceActiveFocus(); } - // Clear error when user starts typing if (connectButton.hasError && event.text && event.text.length > 0) { connectButton.hasError = false; } @@ -191,7 +188,6 @@ Item { target: root.session.network function onShowPasswordDialogChanged(): void { if (root.session.network.showPasswordDialog) { - // Use callLater to ensure focus happens after dialog is fully rendered Qt.callLater(() => { passwordContainer.forceActiveFocus(); passwordContainer.passwordBuffer = ""; @@ -205,7 +201,6 @@ Item { target: root function onVisibleChanged(): void { if (root.visible) { - // Use callLater to ensure focus happens after dialog is fully rendered Qt.callLater(() => { passwordContainer.forceActiveFocus(); }); @@ -383,46 +378,36 @@ Item { return; } - // Clear any previous error hasError = false; - - // Set connecting state connecting = true; enabled = false; text = qsTr("Connecting..."); - // Connect to network NetworkConnection.connectWithPassword(root.network, password, result => { - if (result && result.success) - // Connection successful, monitor will handle the rest - {} else if (result && result.needsPassword) { - // Shouldn't happen since we provided password + if (result && result.success) { + } else if (result && result.needsPassword) { connectionMonitor.stop(); connecting = false; hasError = true; enabled = true; text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; - // Delete the failed connection if (root.network && root.network.ssid) { Nmcli.forgetNetwork(root.network.ssid); } } else { - // Connection failed immediately - show error connectionMonitor.stop(); connecting = false; hasError = true; enabled = true; text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; - // Delete the failed connection if (root.network && root.network.ssid) { Nmcli.forgetNetwork(root.network.ssid); } } }); - // Start monitoring connection connectionMonitor.start(); } } @@ -435,19 +420,14 @@ Item { return; } - // Check if we're connected to the target network (case-insensitive SSID comparison) const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); if (isConnected) { - // Successfully connected - give it a moment for network list to update - // Use Timer for actual delay connectionSuccessTimer.start(); return; } - // Check for connection failures - if pending connection was cleared but we're not connected if (Nmcli.pendingConnection === null && connectButton.connecting) { - // Wait a bit more before giving up (allow time for connection to establish) if (connectionMonitor.repeatCount > 10) { connectionMonitor.stop(); connectButton.connecting = false; @@ -455,7 +435,6 @@ Item { connectButton.enabled = true; connectButton.text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; - // Delete the failed connection if (root.network && root.network.ssid) { Nmcli.forgetNetwork(root.network.ssid); } @@ -486,7 +465,6 @@ Item { id: connectionSuccessTimer interval: 500 onTriggered: { - // Double-check connection is still active if (root.visible && Nmcli.active && Nmcli.active.ssid) { const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); if (stillConnected) { @@ -514,7 +492,6 @@ Item { connectButton.enabled = true; connectButton.text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; - // Delete the failed connection Nmcli.forgetNetwork(ssid); } } diff --git a/modules/controlcenter/state/BluetoothState.qml b/modules/controlcenter/state/BluetoothState.qml index db8c7e1..00497ce 100644 --- a/modules/controlcenter/state/BluetoothState.qml +++ b/modules/controlcenter/state/BluetoothState.qml @@ -4,13 +4,8 @@ import QtQuick QtObject { id: root - // Active selected device property BluetoothDevice active: null - - // Current adapter being used property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter - - // UI state flags property bool editingAdapterName: false property bool fabMenuOpen: false property bool editingDeviceName: false diff --git a/modules/controlcenter/state/EthernetState.qml b/modules/controlcenter/state/EthernetState.qml index 25b243a..c5da7aa 100644 --- a/modules/controlcenter/state/EthernetState.qml +++ b/modules/controlcenter/state/EthernetState.qml @@ -3,7 +3,6 @@ import QtQuick QtObject { id: root - // Active selected ethernet interface property var active: null } diff --git a/modules/controlcenter/state/LauncherState.qml b/modules/controlcenter/state/LauncherState.qml index cd9eeb6..c5da7aa 100644 --- a/modules/controlcenter/state/LauncherState.qml +++ b/modules/controlcenter/state/LauncherState.qml @@ -3,7 +3,6 @@ import QtQuick QtObject { id: root - // Active selected application property var active: null } diff --git a/modules/controlcenter/state/NetworkState.qml b/modules/controlcenter/state/NetworkState.qml index 651a35c..da13e65 100644 --- a/modules/controlcenter/state/NetworkState.qml +++ b/modules/controlcenter/state/NetworkState.qml @@ -3,10 +3,7 @@ import QtQuick QtObject { id: root - // Active selected wireless network property var active: null - - // Password dialog state property bool showPasswordDialog: false property var pendingNetwork: null } diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 1c3adbc..f452b07 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -18,15 +18,10 @@ Item { required property Session session - // Clock property bool clockShowIcon: Config.bar.clock.showIcon ?? true - - // Bar Behavior property bool persistent: Config.bar.persistent ?? true property bool showOnHover: Config.bar.showOnHover ?? true property int dragThreshold: Config.bar.dragThreshold ?? 20 - - // Status Icons property bool showAudio: Config.bar.status.showAudio ?? true property bool showMicrophone: Config.bar.status.showMicrophone ?? true property bool showKbLayout: Config.bar.status.showKbLayout ?? false @@ -34,25 +29,17 @@ Item { property bool showBluetooth: Config.bar.status.showBluetooth ?? true property bool showBattery: Config.bar.status.showBattery ?? true property bool showLockStatus: Config.bar.status.showLockStatus ?? true - - // Tray Settings property bool trayBackground: Config.bar.tray.background ?? false property bool trayCompact: Config.bar.tray.compact ?? false property bool trayRecolour: Config.bar.tray.recolour ?? false - - // Workspaces property int workspacesShown: Config.bar.workspaces.shown ?? 5 property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true - - // Scroll Actions property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true property bool scrollVolume: Config.bar.scrollActions.volume ?? true property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true - - // Popouts property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true property bool popoutTray: Config.bar.popouts.tray ?? true property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true @@ -60,7 +47,6 @@ Item { anchors.fill: parent Component.onCompleted: { - // Update entries if (Config.bar.entries) { entriesModel.clear(); for (let i = 0; i < Config.bar.entries.length; i++) { @@ -74,15 +60,10 @@ Item { } function saveConfig(entryIndex, entryEnabled) { - // Update clock setting Config.bar.clock.showIcon = root.clockShowIcon; - - // Update bar behavior Config.bar.persistent = root.persistent; Config.bar.showOnHover = root.showOnHover; Config.bar.dragThreshold = root.dragThreshold; - - // Update status icons Config.bar.status.showAudio = root.showAudio; Config.bar.status.showMicrophone = root.showMicrophone; Config.bar.status.showKbLayout = root.showKbLayout; @@ -90,35 +71,24 @@ Item { Config.bar.status.showBluetooth = root.showBluetooth; Config.bar.status.showBattery = root.showBattery; Config.bar.status.showLockStatus = root.showLockStatus; - - // Update tray settings Config.bar.tray.background = root.trayBackground; Config.bar.tray.compact = root.trayCompact; Config.bar.tray.recolour = root.trayRecolour; - - // Update workspaces Config.bar.workspaces.shown = root.workspacesShown; Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; Config.bar.workspaces.showWindows = root.workspacesShowWindows; Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; - - // Update scroll actions Config.bar.scrollActions.workspaces = root.scrollWorkspaces; Config.bar.scrollActions.volume = root.scrollVolume; Config.bar.scrollActions.brightness = root.scrollBrightness; - - // Update popouts Config.bar.popouts.activeWindow = root.popoutActiveWindow; Config.bar.popouts.tray = root.popoutTray; Config.bar.popouts.statusIcons = root.popoutStatusIcons; - // Update entries from the model (same approach as clock - use provided value if available) const entries = []; for (let i = 0; i < entriesModel.count; i++) { const entry = entriesModel.get(i); - // If this is the entry being updated, use the provided value (same as clock toggle reads from switch) - // Otherwise use the value from the model let enabled = entry.enabled; if (entryIndex !== undefined && i === entryIndex) { enabled = entryEnabled; @@ -129,8 +99,6 @@ Item { }); } Config.bar.entries = entries; - - // Persist changes to disk Config.save(); } -- cgit v1.2.3-freya From a243b6148b03d3effb7b86993f8ce89911e49b80 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 21:57:55 -0500 Subject: refactor: replaced input fields with SliderInput components --- .../controlcenter/appearance/AppearancePane.qml | 1122 +++----------------- modules/controlcenter/components/DeviceDetails.qml | 2 +- modules/controlcenter/components/SliderInput.qml | 207 ++++ .../components/controls/SliderInput.qml | 179 ++++ modules/controlcenter/network/EthernetDetails.qml | 4 +- modules/controlcenter/network/WirelessDetails.qml | 4 +- modules/controlcenter/taskbar/TaskbarPane.qml | 106 +- 7 files changed, 550 insertions(+), 1074 deletions(-) create mode 100644 modules/controlcenter/components/SliderInput.qml create mode 100644 modules/controlcenter/components/controls/SliderInput.qml diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 5b7e859..dec260d 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -962,98 +962,20 @@ Item { SectionContainer { contentSpacing: Appearance.spacing.normal - ColumnLayout { + SliderInput { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Animation duration scale") - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: animDurationsInput.implicitHeight + Appearance.padding.small * 2 - color: animDurationsInputHover.containsMouse || animDurationsInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: animDurationsInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: animDurationsInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: animDurationsInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 5.0 } - - Component.onCompleted: { - text = (rootPane.animDurationsScale).toFixed(1); - } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 5.0) { - rootPane.animDurationsScale = val; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 5.0) { - text = (rootPane.animDurationsScale).toFixed(1); - } - } - } - } - - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } - - StyledSlider { - id: animDurationsSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0.1 - to: 5.0 - value: rootPane.animDurationsScale - onMoved: { - rootPane.animDurationsScale = animDurationsSlider.value; - if (!animDurationsInput.activeFocus) { - animDurationsInput.text = (animDurationsSlider.value).toFixed(1); - } - rootPane.saveConfig(); - } + + label: qsTr("Animation duration scale") + value: rootPane.animDurationsScale + from: 0.1 + to: 5.0 + decimals: 1 + suffix: "×" + validator: DoubleValidator { bottom: 0.1; top: 5.0 } + + onValueModified: (newValue) => { + rootPane.animDurationsScale = newValue; + rootPane.saveConfig(); } } } @@ -1312,98 +1234,20 @@ Item { SectionContainer { contentSpacing: Appearance.spacing.normal - ColumnLayout { + SliderInput { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Font size scale") - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: fontSizeInput.implicitHeight + Appearance.padding.small * 2 - color: fontSizeInputHover.containsMouse || fontSizeInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: fontSizeInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: fontSizeInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: fontSizeInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.7; top: 1.5 } - - Component.onCompleted: { - text = (rootPane.fontSizeScale).toFixed(1); - } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.7 && val <= 1.5) { - rootPane.fontSizeScale = val; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.7 || val > 1.5) { - text = (rootPane.fontSizeScale).toFixed(1); - } - } - } - } - - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } - - StyledSlider { - id: fontSizeSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0.7 - to: 1.5 - value: rootPane.fontSizeScale - onMoved: { - rootPane.fontSizeScale = fontSizeSlider.value; - if (!fontSizeInput.activeFocus) { - fontSizeInput.text = (fontSizeSlider.value).toFixed(1); - } - rootPane.saveConfig(); - } + + label: qsTr("Font size scale") + value: rootPane.fontSizeScale + from: 0.7 + to: 1.5 + decimals: 2 + suffix: "×" + validator: DoubleValidator { bottom: 0.7; top: 1.5 } + + onValueModified: (newValue) => { + rootPane.fontSizeScale = newValue; + rootPane.saveConfig(); } } } @@ -1417,98 +1261,20 @@ Item { SectionContainer { contentSpacing: Appearance.spacing.normal - ColumnLayout { + SliderInput { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Padding scale") - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: paddingInput.implicitHeight + Appearance.padding.small * 2 - color: paddingInputHover.containsMouse || paddingInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: paddingInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: paddingInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: paddingInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.5; top: 2.0 } - - Component.onCompleted: { - text = (rootPane.paddingScale).toFixed(1); - } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.5 && val <= 2.0) { - rootPane.paddingScale = val; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.5 || val > 2.0) { - text = (rootPane.paddingScale).toFixed(1); - } - } - } - } - - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } - - StyledSlider { - id: paddingSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0.5 - to: 2.0 - value: rootPane.paddingScale - onMoved: { - rootPane.paddingScale = paddingSlider.value; - if (!paddingInput.activeFocus) { - paddingInput.text = (paddingSlider.value).toFixed(1); - } - rootPane.saveConfig(); - } + + label: qsTr("Padding scale") + value: rootPane.paddingScale + from: 0.5 + to: 2.0 + decimals: 1 + suffix: "×" + validator: DoubleValidator { bottom: 0.5; top: 2.0 } + + onValueModified: (newValue) => { + rootPane.paddingScale = newValue; + rootPane.saveConfig(); } } } @@ -1516,98 +1282,20 @@ Item { SectionContainer { contentSpacing: Appearance.spacing.normal - ColumnLayout { + SliderInput { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Rounding scale") - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: roundingInput.implicitHeight + Appearance.padding.small * 2 - color: roundingInputHover.containsMouse || roundingInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: roundingInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: roundingInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: roundingInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 5.0 } - - Component.onCompleted: { - text = (rootPane.roundingScale).toFixed(1); - } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 5.0) { - rootPane.roundingScale = val; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 5.0) { - text = (rootPane.roundingScale).toFixed(1); - } - } - } - } - - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } - - StyledSlider { - id: roundingSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0.1 - to: 5.0 - value: rootPane.roundingScale - onMoved: { - rootPane.roundingScale = roundingSlider.value; - if (!roundingInput.activeFocus) { - roundingInput.text = (roundingSlider.value).toFixed(1); - } - rootPane.saveConfig(); - } + + label: qsTr("Rounding scale") + value: rootPane.roundingScale + from: 0.1 + to: 5.0 + decimals: 1 + suffix: "×" + validator: DoubleValidator { bottom: 0.1; top: 5.0 } + + onValueModified: (newValue) => { + rootPane.roundingScale = newValue; + rootPane.saveConfig(); } } } @@ -1615,98 +1303,20 @@ Item { SectionContainer { contentSpacing: Appearance.spacing.normal - ColumnLayout { + SliderInput { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Spacing scale") - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: spacingInput.implicitHeight + Appearance.padding.small * 2 - color: spacingInputHover.containsMouse || spacingInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: spacingInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: spacingInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: spacingInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 2.0 } - - Component.onCompleted: { - text = (rootPane.spacingScale).toFixed(1); - } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 2.0) { - rootPane.spacingScale = val; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 2.0) { - text = (rootPane.spacingScale).toFixed(1); - } - } - } - } - - StyledText { - text: "×" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } - - StyledSlider { - id: spacingSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0.1 - to: 2.0 - value: rootPane.spacingScale - onMoved: { - rootPane.spacingScale = spacingSlider.value; - if (!spacingInput.activeFocus) { - spacingInput.text = (spacingSlider.value).toFixed(1); - } - rootPane.saveConfig(); - } + + label: qsTr("Spacing scale") + value: rootPane.spacingScale + from: 0.1 + to: 2.0 + decimals: 1 + suffix: "×" + validator: DoubleValidator { bottom: 0.1; top: 2.0 } + + onValueModified: (newValue) => { + rootPane.spacingScale = newValue; + rootPane.saveConfig(); } } } @@ -1729,98 +1339,21 @@ Item { SectionContainer { contentSpacing: Appearance.spacing.normal - ColumnLayout { + SliderInput { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Transparency base") - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: transparencyBaseInput.implicitHeight + Appearance.padding.small * 2 - color: transparencyBaseInputHover.containsMouse || transparencyBaseInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: transparencyBaseInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: transparencyBaseInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: transparencyBaseInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } - - Component.onCompleted: { - text = Math.round(rootPane.transparencyBase * 100).toString(); - } - - onTextChanged: { - if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - rootPane.transparencyBase = val / 100; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(rootPane.transparencyBase * 100).toString(); - } - } - } - } - - StyledText { - text: "%" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } - - StyledSlider { - id: baseSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0 - to: 100 - value: rootPane.transparencyBase * 100 - onMoved: { - rootPane.transparencyBase = baseSlider.value / 100; - if (!transparencyBaseInput.activeFocus) { - transparencyBaseInput.text = Math.round(baseSlider.value).toString(); - } - rootPane.saveConfig(); - } + + label: qsTr("Transparency base") + value: rootPane.transparencyBase * 100 + from: 0 + to: 100 + suffix: "%" + validator: IntValidator { bottom: 0; top: 100 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + rootPane.transparencyBase = newValue / 100; + rootPane.saveConfig(); } } } @@ -1828,98 +1361,21 @@ Item { SectionContainer { contentSpacing: Appearance.spacing.normal - ColumnLayout { + SliderInput { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Transparency layers") - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: transparencyLayersInput.implicitHeight + Appearance.padding.small * 2 - color: transparencyLayersInputHover.containsMouse || transparencyLayersInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: transparencyLayersInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: transparencyLayersInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: transparencyLayersInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } - - Component.onCompleted: { - text = Math.round(rootPane.transparencyLayers * 100).toString(); - } - - onTextChanged: { - if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - rootPane.transparencyLayers = val / 100; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(rootPane.transparencyLayers * 100).toString(); - } - } - } - } - - StyledText { - text: "%" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } - - StyledSlider { - id: layersSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0 - to: 100 - value: rootPane.transparencyLayers * 100 - onMoved: { - rootPane.transparencyLayers = layersSlider.value / 100; - if (!transparencyLayersInput.activeFocus) { - transparencyLayersInput.text = Math.round(layersSlider.value).toString(); - } - rootPane.saveConfig(); - } + + label: qsTr("Transparency layers") + value: rootPane.transparencyLayers * 100 + from: 0 + to: 100 + suffix: "%" + validator: IntValidator { bottom: 0; top: 100 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + rootPane.transparencyLayers = newValue / 100; + rootPane.saveConfig(); } } } @@ -1933,92 +1389,20 @@ Item { SectionContainer { contentSpacing: Appearance.spacing.normal - ColumnLayout { + SliderInput { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Border rounding") - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: borderRoundingInput.implicitHeight + Appearance.padding.small * 2 - color: borderRoundingInputHover.containsMouse || borderRoundingInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: borderRoundingInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: borderRoundingInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: borderRoundingInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 100 } - - Component.onCompleted: { - text = (rootPane.borderRounding).toFixed(1); - } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 100) { - rootPane.borderRounding = val; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 100) { - text = (rootPane.borderRounding).toFixed(1); - } - } - } - } - } - - StyledSlider { - id: borderRoundingSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0.1 - to: 100 - value: rootPane.borderRounding - onMoved: { - rootPane.borderRounding = borderRoundingSlider.value; - if (!borderRoundingInput.activeFocus) { - borderRoundingInput.text = (borderRoundingSlider.value).toFixed(1); - } - rootPane.saveConfig(); - } + + label: qsTr("Border rounding") + value: rootPane.borderRounding + from: 0.1 + to: 100 + decimals: 1 + suffix: "px" + validator: DoubleValidator { bottom: 0.1; top: 100 } + + onValueModified: (newValue) => { + rootPane.borderRounding = newValue; + rootPane.saveConfig(); } } } @@ -2026,92 +1410,20 @@ Item { SectionContainer { contentSpacing: Appearance.spacing.normal - ColumnLayout { + SliderInput { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Border thickness") - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: borderThicknessInput.implicitHeight + Appearance.padding.small * 2 - color: borderThicknessInputHover.containsMouse || borderThicknessInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: borderThicknessInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: borderThicknessInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: borderThicknessInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0.1; top: 100 } - - Component.onCompleted: { - text = (rootPane.borderThickness).toFixed(1); - } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0.1 && val <= 100) { - rootPane.borderThickness = val; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0.1 || val > 100) { - text = (rootPane.borderThickness).toFixed(1); - } - } - } - } - } - - StyledSlider { - id: borderThicknessSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0.1 - to: 100 - value: rootPane.borderThickness - onMoved: { - rootPane.borderThickness = borderThicknessSlider.value; - if (!borderThicknessInput.activeFocus) { - borderThicknessInput.text = (borderThicknessSlider.value).toFixed(1); - } - rootPane.saveConfig(); - } + + label: qsTr("Border thickness") + value: rootPane.borderThickness + from: 0.1 + to: 100 + decimals: 1 + suffix: "px" + validator: DoubleValidator { bottom: 0.1; top: 100 } + + onValueModified: (newValue) => { + rootPane.borderThickness = newValue; + rootPane.saveConfig(); } } } @@ -2168,186 +1480,40 @@ Item { SectionContainer { contentSpacing: Appearance.spacing.normal - ColumnLayout { + SliderInput { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Visualiser rounding") - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: visualiserRoundingInput.implicitHeight + Appearance.padding.small * 2 - color: visualiserRoundingInputHover.containsMouse || visualiserRoundingInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: visualiserRoundingInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: visualiserRoundingInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: visualiserRoundingInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 10 } - - Component.onCompleted: { - text = Math.round(rootPane.visualiserRounding).toString(); - } - - onTextChanged: { - if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 10) { - rootPane.visualiserRounding = val; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 10) { - text = Math.round(rootPane.visualiserRounding).toString(); - } - } - } - } + + label: qsTr("Visualiser rounding") + value: rootPane.visualiserRounding + from: 0 + to: 10 + stepSize: 1 + validator: IntValidator { bottom: 0; top: 10 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + rootPane.visualiserRounding = Math.round(newValue); + rootPane.saveConfig(); } - - StyledSlider { - id: visualiserRoundingSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0 - to: 10 - stepSize: 1 - value: rootPane.visualiserRounding - onMoved: { - rootPane.visualiserRounding = Math.round(visualiserRoundingSlider.value); - if (!visualiserRoundingInput.activeFocus) { - visualiserRoundingInput.text = Math.round(visualiserRoundingSlider.value).toString(); - } - rootPane.saveConfig(); } } - } - } SectionContainer { contentSpacing: Appearance.spacing.normal - ColumnLayout { + SliderInput { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Visualiser spacing") - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: visualiserSpacingInput.implicitHeight + Appearance.padding.small * 2 - color: visualiserSpacingInputHover.containsMouse || visualiserSpacingInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: visualiserSpacingInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: visualiserSpacingInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: visualiserSpacingInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: DoubleValidator { bottom: 0; top: 2 } - - Component.onCompleted: { - text = (rootPane.visualiserSpacing).toFixed(1); - } - - onTextChanged: { - if (activeFocus) { - const val = parseFloat(text); - if (!isNaN(val) && val >= 0 && val <= 2) { - rootPane.visualiserSpacing = val; - rootPane.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseFloat(text); - if (isNaN(val) || val < 0 || val > 2) { - text = (rootPane.visualiserSpacing).toFixed(1); - } - } - } - } - } - - StyledSlider { - id: visualiserSpacingSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0 - to: 2 - value: rootPane.visualiserSpacing - onMoved: { - rootPane.visualiserSpacing = visualiserSpacingSlider.value; - if (!visualiserSpacingInput.activeFocus) { - visualiserSpacingInput.text = (visualiserSpacingSlider.value).toFixed(1); - } - rootPane.saveConfig(); - } + + label: qsTr("Visualiser spacing") + value: rootPane.visualiserSpacing + from: 0 + to: 2 + validator: DoubleValidator { bottom: 0; top: 2 } + + onValueModified: (newValue) => { + rootPane.visualiserSpacing = newValue; + rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index d2e8835..8cc9177 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -12,7 +12,7 @@ import QtQuick.Layouts Item { id: root - required property Session session + property Session session property var device: null property Component headerComponent: null diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml new file mode 100644 index 0000000..3d7cd4d --- /dev/null +++ b/modules/controlcenter/components/SliderInput.qml @@ -0,0 +1,207 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + property string label: "" + property real value: 0 + property real from: 0 + property real to: 100 + property real stepSize: 0 + property var validator: null + property string suffix: "" // Optional suffix text (e.g., "×", "px") + property int decimals: 1 // Number of decimal places to show (default: 1) + property var formatValueFunction: null // Optional custom format function + property var parseValueFunction: null // Optional custom parse function + + function formatValue(val: real): string { + if (formatValueFunction) { + return formatValueFunction(val); + } + // Default format function + // Check if it's an IntValidator (IntValidator doesn't have a 'decimals' property) + if (validator && validator.bottom !== undefined && validator.decimals === undefined) { + return Math.round(val).toString(); + } + // For DoubleValidator or no validator, use the decimals property + return val.toFixed(root.decimals); + } + + function parseValue(text: string): real { + if (parseValueFunction) { + return parseValueFunction(text); + } + // Default parse function + if (validator && validator.bottom !== undefined) { + // Check if it's an integer validator + if (validator.top !== undefined && validator.top === Math.floor(validator.top)) { + return parseInt(text); + } + } + return parseFloat(text); + } + + signal valueModified(real newValue) + + property bool _initialized: false + + spacing: Appearance.spacing.small + + Component.onCompleted: { + // Set initialized flag after a brief delay to allow component to fully load + Qt.callLater(() => { + _initialized = true; + }); + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + visible: root.label !== "" + text: root.label + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 + color: inputHover.containsMouse || inputField.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: inputField.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: inputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: inputField + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: root.validator + + Component.onCompleted: { + // Initialize text without triggering valueModified signal + text = root.formatValue(root.value); + } + + onTextChanged: { + if (activeFocus) { + const val = root.parseValue(text); + if (!isNaN(val)) { + // Validate against validator bounds if available + let isValid = true; + if (root.validator) { + if (root.validator.bottom !== undefined && val < root.validator.bottom) { + isValid = false; + } + if (root.validator.top !== undefined && val > root.validator.top) { + isValid = false; + } + } + + if (isValid) { + root.valueModified(val); + } + } + } + } + + onEditingFinished: { + const val = root.parseValue(text); + let isValid = true; + if (root.validator) { + if (root.validator.bottom !== undefined && val < root.validator.bottom) { + isValid = false; + } + if (root.validator.top !== undefined && val > root.validator.top) { + isValid = false; + } + } + + if (isNaN(val) || !isValid) { + text = root.formatValue(root.value); + } + } + } + } + + StyledText { + visible: root.suffix !== "" + text: root.suffix + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: slider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: root.from + to: root.to + stepSize: root.stepSize + + // Use Binding to allow slider to move freely during dragging + Binding { + target: slider + property: "value" + value: root.value + when: !slider.pressed + } + + onValueChanged: { + // Update input field text in real-time as slider moves during dragging + // Always update when slider value changes (during dragging or external updates) + if (!inputField.activeFocus) { + const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value; + inputField.text = root.formatValue(newValue); + } + } + + onMoved: { + const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value; + root.valueModified(newValue); + if (!inputField.activeFocus) { + inputField.text = root.formatValue(newValue); + } + } + } + + // Update input field when value changes externally (slider is already bound) + onValueChanged: { + // Only update if component is initialized to avoid issues during creation + if (root._initialized && !inputField.activeFocus) { + inputField.text = root.formatValue(root.value); + } + } +} + diff --git a/modules/controlcenter/components/controls/SliderInput.qml b/modules/controlcenter/components/controls/SliderInput.qml new file mode 100644 index 0000000..a114f7f --- /dev/null +++ b/modules/controlcenter/components/controls/SliderInput.qml @@ -0,0 +1,179 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + property string label: "" + property real value: 0 + property real from: 0 + property real to: 100 + property real stepSize: 0 + property var validator: null + property string suffix: "" // Optional suffix text (e.g., "×", "px") + property var formatValueFunction: null // Optional custom format function + property var parseValueFunction: null // Optional custom parse function + + function formatValue(val: real): string { + if (formatValueFunction) { + return formatValueFunction(val); + } + // Default format function + if (validator && validator.bottom !== undefined) { + // Check if it's an integer validator + if (validator.top !== undefined && validator.top === Math.floor(validator.top)) { + return Math.round(val).toString(); + } + } + return val.toFixed(1); + } + + function parseValue(text: string): real { + if (parseValueFunction) { + return parseValueFunction(text); + } + // Default parse function + if (validator && validator.bottom !== undefined) { + // Check if it's an integer validator + if (validator.top !== undefined && validator.top === Math.floor(validator.top)) { + return parseInt(text); + } + } + return parseFloat(text); + } + + signal valueChanged(real newValue) + + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + visible: root.label !== "" + text: root.label + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledRect { + Layout.preferredWidth: 70 + implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 + color: inputHover.containsMouse || inputField.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: inputField.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: inputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + } + + StyledTextField { + id: inputField + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignHCenter + validator: root.validator + + Component.onCompleted: { + text = root.formatValue(root.value); + } + + onTextChanged: { + if (activeFocus) { + const val = root.parseValue(text); + if (!isNaN(val)) { + // Validate against validator bounds if available + let isValid = true; + if (root.validator) { + if (root.validator.bottom !== undefined && val < root.validator.bottom) { + isValid = false; + } + if (root.validator.top !== undefined && val > root.validator.top) { + isValid = false; + } + } + + if (isValid) { + root.valueChanged(val); + } + } + } + } + + onEditingFinished: { + const val = root.parseValue(text); + let isValid = true; + if (root.validator) { + if (root.validator.bottom !== undefined && val < root.validator.bottom) { + isValid = false; + } + if (root.validator.top !== undefined && val > root.validator.top) { + isValid = false; + } + } + + if (isNaN(val) || !isValid) { + text = root.formatValue(root.value); + } + } + } + } + + StyledText { + visible: root.suffix !== "" + text: root.suffix + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: slider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: root.from + to: root.to + stepSize: root.stepSize + value: root.value + + onMoved: { + const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value; + root.valueChanged(newValue); + if (!inputField.activeFocus) { + inputField.text = root.formatValue(newValue); + } + } + } + + // Update input field when value changes externally (slider is already bound) + onValueChanged: { + if (!inputField.activeFocus) { + inputField.text = root.formatValue(root.value); + } + } +} + diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index ad078ec..1cd6c0a 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -15,8 +15,8 @@ DeviceDetails { id: root required property Session session - readonly property var ethernetDevice: session.ethernet.active - + readonly property var ethernetDevice: root.session.ethernet.active + device: ethernetDevice Component.onCompleted: { diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index cf16400..47d42c2 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -17,8 +17,8 @@ DeviceDetails { id: root required property Session session - readonly property var network: session.network.active - + readonly property var network: root.session.network.active + device: network Component.onCompleted: { diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index f452b07..38c1179 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects @@ -520,98 +521,21 @@ Item { SectionContainer { contentSpacing: Appearance.spacing.normal - ColumnLayout { + SliderInput { Layout.fillWidth: true - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - text: qsTr("Drag threshold") - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: dragThresholdInput.implicitHeight + Appearance.padding.small * 2 - color: dragThresholdInputHover.containsMouse || dragThresholdInput.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: dragThresholdInput.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: dragThresholdInputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: dragThresholdInput - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: IntValidator { bottom: 0; top: 100 } - - Component.onCompleted: { - text = root.dragThreshold.toString(); - } - - onTextChanged: { - if (activeFocus) { - const val = parseInt(text); - if (!isNaN(val) && val >= 0 && val <= 100) { - root.dragThreshold = val; - root.saveConfig(); - } - } - } - onEditingFinished: { - const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = root.dragThreshold.toString(); - } - } - } - } - - StyledText { - text: "px" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } - - StyledSlider { - id: dragThresholdSlider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: 0 - to: 100 - value: root.dragThreshold - onMoved: { - root.dragThreshold = Math.round(dragThresholdSlider.value); - if (!dragThresholdInput.activeFocus) { - dragThresholdInput.text = Math.round(dragThresholdSlider.value).toString(); - } - root.saveConfig(); - } + + label: qsTr("Drag threshold") + value: root.dragThreshold + from: 0 + to: 100 + suffix: "px" + validator: IntValidator { bottom: 0; top: 100 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + root.dragThreshold = Math.round(newValue); + root.saveConfig(); } } } -- cgit v1.2.3-freya From bf3a1781d12271a7edb9ead26eb59740f87877fd Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 22:18:10 -0500 Subject: refactor: reorganized AppearancePane sections into dedicated components --- .../controlcenter/appearance/AppearancePane.qml | 515 +-------------------- .../appearance/sections/AnimationsSection.qml | 42 ++ .../appearance/sections/BackgroundSection.qml | 105 +++++ .../appearance/sections/BorderSection.qml | 63 +++ .../appearance/sections/ColorSchemeSection.qml | 147 ++++++ .../appearance/sections/ColorVariantSection.qml | 92 ++++ .../appearance/sections/ScalesSection.qml | 84 ++++ .../appearance/sections/ThemeModeSection.qml | 24 + .../appearance/sections/TransparencySection.qml | 74 +++ modules/controlcenter/network/NetworkingPane.qml | 14 +- 10 files changed, 659 insertions(+), 501 deletions(-) create mode 100644 modules/controlcenter/appearance/sections/AnimationsSection.qml create mode 100644 modules/controlcenter/appearance/sections/BackgroundSection.qml create mode 100644 modules/controlcenter/appearance/sections/BorderSection.qml create mode 100644 modules/controlcenter/appearance/sections/ColorSchemeSection.qml create mode 100644 modules/controlcenter/appearance/sections/ColorVariantSection.qml create mode 100644 modules/controlcenter/appearance/sections/ScalesSection.qml create mode 100644 modules/controlcenter/appearance/sections/ThemeModeSection.qml create mode 100644 modules/controlcenter/appearance/sections/TransparencySection.qml diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index dec260d..d72700f 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import "./sections" import "../../launcher/services" import qs.components import qs.components.controls @@ -680,6 +681,8 @@ Item { anchors.left: parent.left anchors.right: parent.right spacing: Appearance.spacing.small + + readonly property var rootPane: sidebarFlickable.rootPane readonly property bool allSectionsExpanded: themeModeSection.expanded && @@ -724,261 +727,21 @@ Item { } } - CollapsibleSection { + ThemeModeSection { id: themeModeSection - title: qsTr("Theme mode") - description: qsTr("Light or dark theme") - showBackground: true - - SwitchRow { - label: qsTr("Dark mode") - checked: !Colours.currentLight - onToggled: checked => { - Colours.setMode(checked ? "dark" : "light"); - } - } } - CollapsibleSection { + ColorVariantSection { id: colorVariantSection - title: qsTr("Color variant") - description: qsTr("Material theme variant") - showBackground: true - - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small / 2 - - Repeater { - model: M3Variants.list - - delegate: StyledRect { - required property var modelData - - Layout.fillWidth: true - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - const variant = modelData.variant; - - Schemes.currentVariant = variant; - Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); - - Qt.callLater(() => { - reloadTimer.restart(); - }); - } - } - - Timer { - id: reloadTimer - interval: 300 - onTriggered: { - Schemes.reload(); - } - } - - RowLayout { - id: variantRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - - MaterialIcon { - text: modelData.icon - font.pointSize: Appearance.font.size.large - fill: modelData.variant === Schemes.currentVariant ? 1 : 0 - } - - StyledText { - Layout.fillWidth: true - text: modelData.name - font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 - } - - MaterialIcon { - visible: modelData.variant === Schemes.currentVariant - text: "check" - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large - } - } - - implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 - } - } - } } - CollapsibleSection { + ColorSchemeSection { id: colorSchemeSection - title: qsTr("Color scheme") - description: qsTr("Available color schemes") - showBackground: true - - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small / 2 - - Repeater { - model: Schemes.list - - delegate: StyledRect { - required property var modelData - - Layout.fillWidth: true - - readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` - readonly property bool isCurrent: schemeKey === Schemes.currentScheme - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - const name = modelData.name; - const flavour = modelData.flavour; - const schemeKey = `${name} ${flavour}`; - - Schemes.currentScheme = schemeKey; - Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); - - Qt.callLater(() => { - reloadTimer.restart(); - }); - } - } - - Timer { - id: reloadTimer - interval: 300 - onTriggered: { - Schemes.reload(); - } - } - - RowLayout { - id: schemeRow - - anchors.fill: parent - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - - StyledRect { - id: preview - - Layout.alignment: Qt.AlignVCenter - - border.width: 1 - border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) - - color: `#${modelData.colours?.surface}` - radius: Appearance.rounding.full - implicitWidth: iconPlaceholder.implicitWidth - implicitHeight: iconPlaceholder.implicitWidth - - MaterialIcon { - id: iconPlaceholder - visible: false - text: "circle" - font.pointSize: Appearance.font.size.large - } - - Item { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - - implicitWidth: parent.implicitWidth / 2 - clip: true - - StyledRect { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - - implicitWidth: preview.implicitWidth - color: `#${modelData.colours?.primary}` - radius: Appearance.rounding.full - } - } - } - - Column { - Layout.fillWidth: true - spacing: 0 - - StyledText { - text: modelData.flavour ?? "" - font.pointSize: Appearance.font.size.normal - } - - StyledText { - text: modelData.name ?? "" - font.pointSize: Appearance.font.size.small - color: Colours.palette.m3outline - - elide: Text.ElideRight - anchors.left: parent.left - anchors.right: parent.right - } - } - - Loader { - active: isCurrent - asynchronous: true - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } - } - } - - implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 - } - } - } } - CollapsibleSection { + AnimationsSection { id: animationsSection - title: qsTr("Animations") - showBackground: true - - SectionContainer { - contentSpacing: Appearance.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Animation duration scale") - value: rootPane.animDurationsScale - from: 0.1 - to: 5.0 - decimals: 1 - suffix: "×" - validator: DoubleValidator { bottom: 0.1; top: 5.0 } - - onValueModified: (newValue) => { - rootPane.animDurationsScale = newValue; - rootPane.saveConfig(); - } - } - } + rootPane: sidebarFlickable.rootPane } CollapsibleSection { @@ -1253,270 +1016,24 @@ Item { } } - CollapsibleSection { + ScalesSection { id: scalesSection - title: qsTr("Scales") - showBackground: true - - SectionContainer { - contentSpacing: Appearance.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Padding scale") - value: rootPane.paddingScale - from: 0.5 - to: 2.0 - decimals: 1 - suffix: "×" - validator: DoubleValidator { bottom: 0.5; top: 2.0 } - - onValueModified: (newValue) => { - rootPane.paddingScale = newValue; - rootPane.saveConfig(); - } - } - } - - SectionContainer { - contentSpacing: Appearance.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Rounding scale") - value: rootPane.roundingScale - from: 0.1 - to: 5.0 - decimals: 1 - suffix: "×" - validator: DoubleValidator { bottom: 0.1; top: 5.0 } - - onValueModified: (newValue) => { - rootPane.roundingScale = newValue; - rootPane.saveConfig(); - } - } - } - - SectionContainer { - contentSpacing: Appearance.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Spacing scale") - value: rootPane.spacingScale - from: 0.1 - to: 2.0 - decimals: 1 - suffix: "×" - validator: DoubleValidator { bottom: 0.1; top: 2.0 } - - onValueModified: (newValue) => { - rootPane.spacingScale = newValue; - rootPane.saveConfig(); - } - } - } + rootPane: sidebarFlickable.rootPane } - CollapsibleSection { + TransparencySection { id: transparencySection - title: qsTr("Transparency") - showBackground: true - - SwitchRow { - label: qsTr("Transparency enabled") - checked: rootPane.transparencyEnabled - onToggled: checked => { - rootPane.transparencyEnabled = checked; - rootPane.saveConfig(); - } - } - - SectionContainer { - contentSpacing: Appearance.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Transparency base") - value: rootPane.transparencyBase * 100 - from: 0 - to: 100 - suffix: "%" - validator: IntValidator { bottom: 0; top: 100 } - formatValueFunction: (val) => Math.round(val).toString() - parseValueFunction: (text) => parseInt(text) - - onValueModified: (newValue) => { - rootPane.transparencyBase = newValue / 100; - rootPane.saveConfig(); - } - } - } - - SectionContainer { - contentSpacing: Appearance.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Transparency layers") - value: rootPane.transparencyLayers * 100 - from: 0 - to: 100 - suffix: "%" - validator: IntValidator { bottom: 0; top: 100 } - formatValueFunction: (val) => Math.round(val).toString() - parseValueFunction: (text) => parseInt(text) - - onValueModified: (newValue) => { - rootPane.transparencyLayers = newValue / 100; - rootPane.saveConfig(); - } - } - } + rootPane: sidebarFlickable.rootPane } - CollapsibleSection { + BorderSection { id: borderSection - title: qsTr("Border") - showBackground: true - - SectionContainer { - contentSpacing: Appearance.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Border rounding") - value: rootPane.borderRounding - from: 0.1 - to: 100 - decimals: 1 - suffix: "px" - validator: DoubleValidator { bottom: 0.1; top: 100 } - - onValueModified: (newValue) => { - rootPane.borderRounding = newValue; - rootPane.saveConfig(); - } - } - } - - SectionContainer { - contentSpacing: Appearance.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Border thickness") - value: rootPane.borderThickness - from: 0.1 - to: 100 - decimals: 1 - suffix: "px" - validator: DoubleValidator { bottom: 0.1; top: 100 } - - onValueModified: (newValue) => { - rootPane.borderThickness = newValue; - rootPane.saveConfig(); - } - } - } + rootPane: sidebarFlickable.rootPane } - CollapsibleSection { + BackgroundSection { id: backgroundSection - title: qsTr("Background") - showBackground: true - - SwitchRow { - label: qsTr("Desktop clock") - checked: rootPane.desktopClockEnabled - onToggled: checked => { - rootPane.desktopClockEnabled = checked; - rootPane.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Background enabled") - checked: rootPane.backgroundEnabled - onToggled: checked => { - rootPane.backgroundEnabled = checked; - rootPane.saveConfig(); - } - } - - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Visualiser") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - SwitchRow { - label: qsTr("Visualiser enabled") - checked: rootPane.visualiserEnabled - onToggled: checked => { - rootPane.visualiserEnabled = checked; - rootPane.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Visualiser auto hide") - checked: rootPane.visualiserAutoHide - onToggled: checked => { - rootPane.visualiserAutoHide = checked; - rootPane.saveConfig(); - } - } - - SectionContainer { - contentSpacing: Appearance.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Visualiser rounding") - value: rootPane.visualiserRounding - from: 0 - to: 10 - stepSize: 1 - validator: IntValidator { bottom: 0; top: 10 } - formatValueFunction: (val) => Math.round(val).toString() - parseValueFunction: (text) => parseInt(text) - - onValueModified: (newValue) => { - rootPane.visualiserRounding = Math.round(newValue); - rootPane.saveConfig(); - } - } - } - - SectionContainer { - contentSpacing: Appearance.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Visualiser spacing") - value: rootPane.visualiserSpacing - from: 0 - to: 2 - validator: DoubleValidator { bottom: 0; top: 2 } - - onValueModified: (newValue) => { - rootPane.visualiserSpacing = newValue; - rootPane.saveConfig(); - } - } - } + rootPane: sidebarFlickable.rootPane } } } diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml new file mode 100644 index 0000000..03fc2b1 --- /dev/null +++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml @@ -0,0 +1,42 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + id: root + + required property var rootPane + + title: qsTr("Animations") + showBackground: true + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Animation duration scale") + value: rootPane.animDurationsScale + from: 0.1 + to: 5.0 + decimals: 1 + suffix: "×" + validator: DoubleValidator { bottom: 0.1; top: 5.0 } + + onValueModified: (newValue) => { + rootPane.animDurationsScale = newValue; + rootPane.saveConfig(); + } + } + } +} + diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml new file mode 100644 index 0000000..8754e73 --- /dev/null +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -0,0 +1,105 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + id: root + + required property var rootPane + + title: qsTr("Background") + showBackground: true + + SwitchRow { + label: qsTr("Desktop clock") + checked: rootPane.desktopClockEnabled + onToggled: checked => { + rootPane.desktopClockEnabled = checked; + rootPane.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Background enabled") + checked: rootPane.backgroundEnabled + onToggled: checked => { + rootPane.backgroundEnabled = checked; + rootPane.saveConfig(); + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Visualiser") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + SwitchRow { + label: qsTr("Visualiser enabled") + checked: rootPane.visualiserEnabled + onToggled: checked => { + rootPane.visualiserEnabled = checked; + rootPane.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Visualiser auto hide") + checked: rootPane.visualiserAutoHide + onToggled: checked => { + rootPane.visualiserAutoHide = checked; + rootPane.saveConfig(); + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Visualiser rounding") + value: rootPane.visualiserRounding + from: 0 + to: 10 + stepSize: 1 + validator: IntValidator { bottom: 0; top: 10 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + rootPane.visualiserRounding = Math.round(newValue); + rootPane.saveConfig(); + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Visualiser spacing") + value: rootPane.visualiserSpacing + from: 0 + to: 2 + validator: DoubleValidator { bottom: 0; top: 2 } + + onValueModified: (newValue) => { + rootPane.visualiserSpacing = newValue; + rootPane.saveConfig(); + } + } + } +} + diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml new file mode 100644 index 0000000..dae26c3 --- /dev/null +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -0,0 +1,63 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + id: root + + required property var rootPane + + title: qsTr("Border") + showBackground: true + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Border rounding") + value: rootPane.borderRounding + from: 0.1 + to: 100 + decimals: 1 + suffix: "px" + validator: DoubleValidator { bottom: 0.1; top: 100 } + + onValueModified: (newValue) => { + rootPane.borderRounding = newValue; + rootPane.saveConfig(); + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Border thickness") + value: rootPane.borderThickness + from: 0.1 + to: 100 + decimals: 1 + suffix: "px" + validator: DoubleValidator { bottom: 0.1; top: 100 } + + onValueModified: (newValue) => { + rootPane.borderThickness = newValue; + rootPane.saveConfig(); + } + } + } +} + diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml new file mode 100644 index 0000000..c0e5eb5 --- /dev/null +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -0,0 +1,147 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../../launcher/services" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + title: qsTr("Color scheme") + description: qsTr("Available color schemes") + showBackground: true + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small / 2 + + Repeater { + model: Schemes.list + + delegate: StyledRect { + required property var modelData + + Layout.fillWidth: true + + readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` + readonly property bool isCurrent: schemeKey === Schemes.currentScheme + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + const name = modelData.name; + const flavour = modelData.flavour; + const schemeKey = `${name} ${flavour}`; + + Schemes.currentScheme = schemeKey; + Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); + + Qt.callLater(() => { + reloadTimer.restart(); + }); + } + } + + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); + } + } + + RowLayout { + id: schemeRow + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + id: preview + + Layout.alignment: Qt.AlignVCenter + + border.width: 1 + border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) + + color: `#${modelData.colours?.surface}` + radius: Appearance.rounding.full + implicitWidth: iconPlaceholder.implicitWidth + implicitHeight: iconPlaceholder.implicitWidth + + MaterialIcon { + id: iconPlaceholder + visible: false + text: "circle" + font.pointSize: Appearance.font.size.large + } + + Item { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + implicitWidth: parent.implicitWidth / 2 + clip: true + + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + implicitWidth: preview.implicitWidth + color: `#${modelData.colours?.primary}` + radius: Appearance.rounding.full + } + } + } + + Column { + Layout.fillWidth: true + spacing: 0 + + StyledText { + text: modelData.flavour ?? "" + font.pointSize: Appearance.font.size.normal + } + + StyledText { + text: modelData.name ?? "" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline + + elide: Text.ElideRight + anchors.left: parent.left + anchors.right: parent.right + } + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } + + implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } +} + diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml new file mode 100644 index 0000000..98c3d7c --- /dev/null +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -0,0 +1,92 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../../launcher/services" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + title: qsTr("Color variant") + description: qsTr("Material theme variant") + showBackground: true + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small / 2 + + Repeater { + model: M3Variants.list + + delegate: StyledRect { + required property var modelData + + Layout.fillWidth: true + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + const variant = modelData.variant; + + Schemes.currentVariant = variant; + Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); + + Qt.callLater(() => { + reloadTimer.restart(); + }); + } + } + + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); + } + } + + RowLayout { + id: variantRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + MaterialIcon { + text: modelData.icon + font.pointSize: Appearance.font.size.large + fill: modelData.variant === Schemes.currentVariant ? 1 : 0 + } + + StyledText { + Layout.fillWidth: true + text: modelData.name + font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 + } + + MaterialIcon { + visible: modelData.variant === Schemes.currentVariant + text: "check" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.large + } + } + + implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } +} + diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml new file mode 100644 index 0000000..f74923b --- /dev/null +++ b/modules/controlcenter/appearance/sections/ScalesSection.qml @@ -0,0 +1,84 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + id: root + + required property var rootPane + + title: qsTr("Scales") + showBackground: true + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Padding scale") + value: rootPane.paddingScale + from: 0.5 + to: 2.0 + decimals: 1 + suffix: "×" + validator: DoubleValidator { bottom: 0.5; top: 2.0 } + + onValueModified: (newValue) => { + rootPane.paddingScale = newValue; + rootPane.saveConfig(); + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Rounding scale") + value: rootPane.roundingScale + from: 0.1 + to: 5.0 + decimals: 1 + suffix: "×" + validator: DoubleValidator { bottom: 0.1; top: 5.0 } + + onValueModified: (newValue) => { + rootPane.roundingScale = newValue; + rootPane.saveConfig(); + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Spacing scale") + value: rootPane.spacingScale + from: 0.1 + to: 2.0 + decimals: 1 + suffix: "×" + validator: DoubleValidator { bottom: 0.1; top: 2.0 } + + onValueModified: (newValue) => { + rootPane.spacingScale = newValue; + rootPane.saveConfig(); + } + } + } +} + diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml new file mode 100644 index 0000000..c136437 --- /dev/null +++ b/modules/controlcenter/appearance/sections/ThemeModeSection.qml @@ -0,0 +1,24 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick + +CollapsibleSection { + title: qsTr("Theme mode") + description: qsTr("Light or dark theme") + showBackground: true + + SwitchRow { + label: qsTr("Dark mode") + checked: !Colours.currentLight + onToggled: checked => { + Colours.setMode(checked ? "dark" : "light"); + } + } +} + diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml new file mode 100644 index 0000000..c9dbfb8 --- /dev/null +++ b/modules/controlcenter/appearance/sections/TransparencySection.qml @@ -0,0 +1,74 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + id: root + + required property var rootPane + + title: qsTr("Transparency") + showBackground: true + + SwitchRow { + label: qsTr("Transparency enabled") + checked: rootPane.transparencyEnabled + onToggled: checked => { + rootPane.transparencyEnabled = checked; + rootPane.saveConfig(); + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Transparency base") + value: rootPane.transparencyBase * 100 + from: 0 + to: 100 + suffix: "%" + validator: IntValidator { bottom: 0; top: 100 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + rootPane.transparencyBase = newValue / 100; + rootPane.saveConfig(); + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Transparency layers") + value: rootPane.transparencyLayers * 100 + from: 0 + to: 100 + suffix: "%" + validator: IntValidator { bottom: 0; top: 100 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + rootPane.transparencyLayers = newValue / 100; + rootPane.saveConfig(); + } + } + } +} + diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 4446428..b430cce 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -175,16 +175,26 @@ Item { Connections { target: root.session.ethernet function onActiveChanged() { + // Clear wireless when ethernet is selected + if (root.session.ethernet.active && root.session.network.active) { + root.session.network.active = null; + return; // Let the network.onActiveChanged handle the update + } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); - rightPaneItem.paneId = rightPaneItem.ethernetPane ? ("eth:" + (rightPaneItem.ethernetPane.interface || "")) : (rightPaneItem.wirelessPane ? ("wifi:" + (rightPaneItem.wirelessPane.ssid || rightPaneItem.wirelessPane.bssid || "")) : "settings"); + // paneId will automatically update via property binding } } Connections { target: root.session.network function onActiveChanged() { + // Clear ethernet when wireless is selected + if (root.session.network.active && root.session.ethernet.active) { + root.session.ethernet.active = null; + return; // Let the ethernet.onActiveChanged handle the update + } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); - rightPaneItem.paneId = rightPaneItem.ethernetPane ? ("eth:" + (rightPaneItem.ethernetPane.interface || "")) : (rightPaneItem.wirelessPane ? ("wifi:" + (rightPaneItem.wirelessPane.ssid || rightPaneItem.wirelessPane.bssid || "")) : "settings"); + // paneId will automatically update via property binding } } -- cgit v1.2.3-freya From ecc1226a0ec470faed9ab90f5725dc99858bcb73 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 22:25:12 -0500 Subject: refactor: replaced CollapsibleSection with FontsSection --- .../controlcenter/appearance/AppearancePane.qml | 271 +------------------ .../appearance/sections/FontsSection.qml | 286 +++++++++++++++++++++ 2 files changed, 288 insertions(+), 269 deletions(-) create mode 100644 modules/controlcenter/appearance/sections/FontsSection.qml diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index d72700f..36028d2 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -744,276 +744,9 @@ Item { rootPane: sidebarFlickable.rootPane } - CollapsibleSection { + FontsSection { id: fontsSection - title: qsTr("Fonts") - showBackground: true - - CollapsibleSection { - id: materialFontSection - title: qsTr("Material font family") - expanded: true - showBackground: true - nested: true - - Loader { - id: materialFontLoader - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: materialFontSection.expanded - - sourceComponent: StyledListView { - id: materialFontList - property alias contentHeight: materialFontList.contentHeight - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() - - StyledScrollBar.vertical: StyledScrollBar { - flickable: materialFontList - } - - delegate: StyledRect { - required property string modelData - required property int index - - width: ListView.view.width - - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMaterial = modelData; - rootPane.saveConfig(); - } - } - - RowLayout { - id: fontFamilyMaterialRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - Loader { - active: isCurrent - asynchronous: true - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } - } - } - - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 - } - } - - } - } - - CollapsibleSection { - id: monoFontSection - title: qsTr("Monospace font family") - expanded: false - showBackground: true - nested: true - - Loader { - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: monoFontSection.expanded - - sourceComponent: StyledListView { - id: monoFontList - property alias contentHeight: monoFontList.contentHeight - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() - - StyledScrollBar.vertical: StyledScrollBar { - flickable: monoFontList - } - - delegate: StyledRect { - required property string modelData - required property int index - - width: ListView.view.width - - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - rootPane.fontFamilyMono = modelData; - rootPane.saveConfig(); - } - } - - RowLayout { - id: fontFamilyMonoRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - Loader { - active: isCurrent - asynchronous: true - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } - } - } - - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 - } - } - } - } - - CollapsibleSection { - id: sansFontSection - title: qsTr("Sans-serif font family") - expanded: false - showBackground: true - nested: true - - Loader { - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: sansFontSection.expanded - - sourceComponent: StyledListView { - id: sansFontList - property alias contentHeight: sansFontList.contentHeight - - clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() - - StyledScrollBar.vertical: StyledScrollBar { - flickable: sansFontList - } - - delegate: StyledRect { - required property string modelData - required property int index - - width: ListView.view.width - - readonly property bool isCurrent: modelData === rootPane.fontFamilySans - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary - - StateLayer { - function onClicked(): void { - rootPane.fontFamilySans = modelData; - rootPane.saveConfig(); - } - } - - RowLayout { - id: fontFamilySansRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - - spacing: Appearance.spacing.normal - - StyledText { - text: modelData - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - Loader { - active: isCurrent - asynchronous: true - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large - } - } - } - - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 - } - } - } - } - - SectionContainer { - contentSpacing: Appearance.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Font size scale") - value: rootPane.fontSizeScale - from: 0.7 - to: 1.5 - decimals: 2 - suffix: "×" - validator: DoubleValidator { bottom: 0.7; top: 1.5 } - - onValueModified: (newValue) => { - rootPane.fontSizeScale = newValue; - rootPane.saveConfig(); - } - } - } + rootPane: sidebarFlickable.rootPane } ScalesSection { diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml new file mode 100644 index 0000000..57b10ff --- /dev/null +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -0,0 +1,286 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + id: root + + required property var rootPane + + title: qsTr("Fonts") + showBackground: true + + CollapsibleSection { + id: materialFontSection + title: qsTr("Material font family") + expanded: true + showBackground: true + nested: true + + Loader { + id: materialFontLoader + Layout.fillWidth: true + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: materialFontSection.expanded + + sourceComponent: StyledListView { + id: materialFontList + property alias contentHeight: materialFontList.contentHeight + + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() + + StyledScrollBar.vertical: StyledScrollBar { + flickable: materialFontList + } + + delegate: StyledRect { + required property string modelData + required property int index + + width: ListView.view.width + + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMaterial = modelData; + rootPane.saveConfig(); + } + } + + RowLayout { + id: fontFamilyMaterialRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } + + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } + } + + CollapsibleSection { + id: monoFontSection + title: qsTr("Monospace font family") + expanded: false + showBackground: true + nested: true + + Loader { + Layout.fillWidth: true + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: monoFontSection.expanded + + sourceComponent: StyledListView { + id: monoFontList + property alias contentHeight: monoFontList.contentHeight + + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() + + StyledScrollBar.vertical: StyledScrollBar { + flickable: monoFontList + } + + delegate: StyledRect { + required property string modelData + required property int index + + width: ListView.view.width + + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMono = modelData; + rootPane.saveConfig(); + } + } + + RowLayout { + id: fontFamilyMonoRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } + + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } + } + + CollapsibleSection { + id: sansFontSection + title: qsTr("Sans-serif font family") + expanded: false + showBackground: true + nested: true + + Loader { + Layout.fillWidth: true + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: sansFontSection.expanded + + sourceComponent: StyledListView { + id: sansFontList + property alias contentHeight: sansFontList.contentHeight + + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() + + StyledScrollBar.vertical: StyledScrollBar { + flickable: sansFontList + } + + delegate: StyledRect { + required property string modelData + required property int index + + width: ListView.view.width + + readonly property bool isCurrent: modelData === rootPane.fontFamilySans + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + rootPane.fontFamilySans = modelData; + rootPane.saveConfig(); + } + } + + RowLayout { + id: fontFamilySansRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } + + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Font size scale") + value: rootPane.fontSizeScale + from: 0.7 + to: 1.5 + decimals: 2 + suffix: "×" + validator: DoubleValidator { bottom: 0.7; top: 1.5 } + + onValueModified: (newValue) => { + rootPane.fontSizeScale = newValue; + rootPane.saveConfig(); + } + } + } +} + -- cgit v1.2.3-freya From 324b32c0c2ea1df2229dc341f3d120733a9320ae Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 22:34:06 -0500 Subject: refactor: update ConnectedButtonGroup --- .../controlcenter/taskbar/ConnectedButtonGroup.qml | 105 +++------------------ 1 file changed, 13 insertions(+), 92 deletions(-) diff --git a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml b/modules/controlcenter/taskbar/ConnectedButtonGroup.qml index af386c3..bf3a97f 100644 --- a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml +++ b/modules/controlcenter/taskbar/ConnectedButtonGroup.qml @@ -54,100 +54,20 @@ StyledRect { Layout.fillWidth: true text: modelData.label - property bool isChecked: false + property bool _checked: false - // Initialize from root property - Component.onCompleted: { - if (root.rootItem && modelData.propertyName) { - isChecked = root.rootItem[modelData.propertyName]; - } - } - - checked: isChecked + checked: _checked toggle: false type: TextButton.Tonal - // Listen for property changes on rootItem - Connections { - target: root.rootItem - enabled: root.rootItem !== null && modelData.propertyName !== undefined - - function onShowAudioChanged() { - if (modelData.propertyName === "showAudio") { - button.isChecked = root.rootItem.showAudio; - } - } - - function onShowMicrophoneChanged() { - if (modelData.propertyName === "showMicrophone") { - button.isChecked = root.rootItem.showMicrophone; - } - } - - function onShowKbLayoutChanged() { - if (modelData.propertyName === "showKbLayout") { - button.isChecked = root.rootItem.showKbLayout; - } - } - - function onShowNetworkChanged() { - if (modelData.propertyName === "showNetwork") { - button.isChecked = root.rootItem.showNetwork; - } - } - - function onShowBluetoothChanged() { - if (modelData.propertyName === "showBluetooth") { - button.isChecked = root.rootItem.showBluetooth; - } - } - - function onShowBatteryChanged() { - if (modelData.propertyName === "showBattery") { - button.isChecked = root.rootItem.showBattery; - } - } - - function onShowLockStatusChanged() { - if (modelData.propertyName === "showLockStatus") { - button.isChecked = root.rootItem.showLockStatus; - } - } - - function onTrayBackgroundChanged() { - if (modelData.propertyName === "trayBackground") { - button.isChecked = root.rootItem.trayBackground; - } - } - - function onTrayCompactChanged() { - if (modelData.propertyName === "trayCompact") { - button.isChecked = root.rootItem.trayCompact; - } - } - - function onTrayRecolourChanged() { - if (modelData.propertyName === "trayRecolour") { - button.isChecked = root.rootItem.trayRecolour; - } - } - - function onScrollWorkspacesChanged() { - if (modelData.propertyName === "scrollWorkspaces") { - button.isChecked = root.rootItem.scrollWorkspaces; - } - } - - function onScrollVolumeChanged() { - if (modelData.propertyName === "scrollVolume") { - button.isChecked = root.rootItem.scrollVolume; - } - } - - function onScrollBrightnessChanged() { - if (modelData.propertyName === "scrollBrightness") { - button.isChecked = root.rootItem.scrollBrightness; - } + // Create binding in Component.onCompleted + Component.onCompleted: { + if (root.rootItem && modelData.propertyName) { + const propName = modelData.propertyName; + const rootItem = root.rootItem; + _checked = Qt.binding(function() { + return rootItem[propName] ?? false; + }); } } @@ -162,8 +82,9 @@ StyledRect { Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) onClicked: { - if (modelData.onToggled) { - modelData.onToggled(!checked); + if (modelData.onToggled && root.rootItem && modelData.propertyName) { + const currentValue = root.rootItem[modelData.propertyName] ?? false; + modelData.onToggled(!currentValue); } } -- cgit v1.2.3-freya From e781326da40f919e736702e18081ab3418c92741 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Wed, 19 Nov 2025 22:41:07 -0500 Subject: refactor: replaced wallpaper logic with WallpaperGrid component --- .../controlcenter/appearance/AppearancePane.qml | 515 +------------------- modules/controlcenter/components/WallpaperGrid.qml | 516 +++++++++++++++++++++ 2 files changed, 518 insertions(+), 513 deletions(-) create mode 100644 modules/controlcenter/components/WallpaperGrid.qml diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 36028d2..aea0085 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -140,519 +140,8 @@ Item { } } - // Stop lazy loading when loader becomes inactive - onActiveChanged: { - if (!active && wallpaperLoader.item) { - const container = wallpaperLoader.item; - 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; - } - } - - QtObject { - id: lazyModel - - property var sourceList: null - property int loadedCount: 0 - property int visibleCount: 0 - property int totalCount: 0 - - function initialize(list) { - sourceList = list; - totalCount = list ? list.length : 0; - 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) { - 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)) - - 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 - - ListModel { - id: wallpaperListModel - } - - model: wallpaperListModel - - Connections { - target: lazyModel - function onVisibleCountChanged(): void { - if (!lazyModel || !lazyModel.sourceList) return; - - const newCount = lazyModel.visibleCount; - const currentCount = wallpaperListModel.count; - - 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]}); - } - - 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); - - 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); - - 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; - }); - } - } - } - } - - 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; - } - } - } - } + sourceComponent: WallpaperGrid { + session: root.session } } } diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml new file mode 100644 index 0000000..2fa4242 --- /dev/null +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -0,0 +1,516 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.images +import qs.services +import qs.config +import Caelestia.Models +import QtQuick + +Item { + id: root + + required property Session session + + 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; + } + } + + QtObject { + id: lazyModel + + property var sourceList: null + property int loadedCount: 0 + property int visibleCount: 0 + property int totalCount: 0 + + function initialize(list) { + sourceList = list; + totalCount = list ? list.length : 0; + 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) { + 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)) + + 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 + + ListModel { + id: wallpaperListModel + } + + model: wallpaperListModel + + Connections { + target: lazyModel + function onVisibleCountChanged(): void { + if (!lazyModel || !lazyModel.sourceList) return; + + const newCount = lazyModel.visibleCount; + const currentCount = wallpaperListModel.count; + + if (newCount > currentCount) { + const flickable = root.parentFlickable; + const oldScrollY = flickable ? flickable.contentY : 0; + + for (let i = currentCount; i < newCount; i++) { + wallpaperListModel.append({modelData: lazyModel.sourceList[i]}); + } + + 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: root.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 = root.parentFlickable; + if (!flickable) return; + + const gridY = root.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); + + 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 = root.parentFlickable; + if (!flickable || !lazyModel || !lazyModel.sourceList) return; + + const gridY = root.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); + + 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; + }); + } + } + } + } + + 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; + } + } + } + } +} + -- cgit v1.2.3-freya From a15554ffde440ef85a93182fc765f3e006999cde Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 20 Nov 2025 08:13:59 -0500 Subject: refactor: EthernetList and WirelessList components removed border properties --- modules/controlcenter/network/EthernetList.qml | 2 - modules/controlcenter/network/WirelessList.qml | 82 ++++++++++++++++++++------ 2 files changed, 64 insertions(+), 20 deletions(-) diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index ea3ece5..bdc43e8 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -62,8 +62,6 @@ DeviceList { color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal - border.width: root.activeItem === modelData ? 1 : 0 - border.color: Colours.palette.m3primary StateLayer { function onClicked(): void { diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 9dabe9d..43d56d2 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -6,6 +6,7 @@ import "." import qs.components import qs.components.controls import qs.components.containers +import qs.components.effects import qs.services import qs.config import qs.utils @@ -106,8 +107,6 @@ DeviceList { color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal - border.width: root.activeItem === modelData ? 1 : 0 - border.color: Colours.palette.m3primary StateLayer { function onClicked(): void { @@ -139,32 +138,79 @@ DeviceList { id: icon anchors.centerIn: parent - text: modelData.isSecure ? "lock" : "wifi" + text: Icons.getNetworkIcon(modelData.strength) font.pointSize: Appearance.font.size.large fill: modelData.active ? 1 : 0 color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } + + StyledRect { + id: lockBadge + + visible: modelData.isSecure + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: -Appearance.padding.smaller / 2 + + implicitWidth: lockIconSize + Appearance.padding.smaller + implicitHeight: lockIconSize + Appearance.padding.smaller + radius: Appearance.rounding.full + color: Colours.palette.m3secondaryContainer + + readonly property real lockIconSize: lockIcon.implicitWidth + + Elevation { + anchors.fill: parent + radius: parent.radius + z: -1 + level: 2 + } + + MaterialIcon { + id: lockIcon + + anchors.centerIn: parent + text: "lock" + font.pointSize: Appearance.font.size.small + fill: 1 + color: Colours.palette.m3onSurface + } + } } - StyledText { + ColumnLayout { Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - text: modelData.ssid || qsTr("Unknown") - } + spacing: 0 - StyledText { - text: modelData.active ? qsTr("Connected") : (modelData.isSecure ? qsTr("Secured") : qsTr("Open")) - color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: modelData.active ? 500 : 400 - } + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + + text: modelData.ssid || qsTr("Unknown") + } - StyledText { - text: qsTr("%1%").arg(modelData.strength) - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + StyledText { + Layout.fillWidth: true + text: { + if (modelData.active) return qsTr("Connected"); + if (modelData.isSecure && modelData.security && modelData.security.length > 0) { + return modelData.security; + } + if (modelData.isSecure) return qsTr("Secured"); + return qsTr("Open"); + } + color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.active ? 500 : 400 + elide: Text.ElideRight + } + } } StyledRect { -- cgit v1.2.3-freya From ea9ab75db1903fd3c8117e63ae9f55b53d2142e9 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 20 Nov 2025 08:18:29 -0500 Subject: refactor: EthernetList correction on selected style --- modules/controlcenter/network/EthernetList.qml | 54 ++++++++++++++++++-------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index bdc43e8..e0d4b3f 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -56,14 +56,20 @@ DeviceList { delegate: Component { StyledRect { + id: ethernetItem + required property var modelData + readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface width: ListView.view ? ListView.view.width : undefined + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal StateLayer { + id: stateLayer + function onClicked(): void { root.session.ethernet.active = modelData; } @@ -72,9 +78,7 @@ DeviceList { RowLayout { id: rowLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + anchors.fill: parent anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal @@ -86,6 +90,12 @@ DeviceList { radius: Appearance.rounding.normal color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + StyledRect { + anchors.fill: parent + radius: parent.radius + color: Qt.alpha(modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) + } + MaterialIcon { id: icon @@ -94,25 +104,36 @@ DeviceList { font.pointSize: Appearance.font.size.large fill: modelData.connected ? 1 : 0 color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + + Behavior on fill { + Anim {} + } } } - StyledText { + ColumnLayout { Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 - text: modelData.interface || qsTr("Unknown") - } + spacing: 0 - StyledText { - text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") - color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: modelData.connected ? 500 : 400 + StyledText { + Layout.fillWidth: true + text: modelData.interface || qsTr("Unknown") + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + elide: Text.ElideRight + } } StyledRect { + id: connectBtn + implicitWidth: implicitHeight implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 @@ -120,6 +141,8 @@ DeviceList { color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) StateLayer { + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + function onClicked(): void { if (modelData.connected && modelData.connection) { Nmcli.disconnectEthernet(modelData.connection, () => {}); @@ -133,13 +156,12 @@ DeviceList { id: connectIcon anchors.centerIn: parent + animate: true text: modelData.connected ? "link_off" : "link" color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } } - - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } -- cgit v1.2.3-freya From ae74eabfca2fa177235d25b364dc7dafe0f539d2 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 20 Nov 2025 08:24:19 -0500 Subject: refactor: EthernetList styling --- modules/controlcenter/network/EthernetList.qml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index e0d4b3f..4f4dc8a 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -122,12 +122,18 @@ DeviceList { elide: Text.ElideRight } - StyledText { + RowLayout { Layout.fillWidth: true - text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - elide: Text.ElideRight + spacing: Appearance.spacing.smaller + + StyledText { + Layout.fillWidth: true + text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.connected ? 500 : 400 + elide: Text.ElideRight + } } } -- cgit v1.2.3-freya From 5a369702d1e227df0842ad31d8d93710ad068add Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 20 Nov 2025 08:36:10 -0500 Subject: controlcenter: updated wireless icons --- modules/controlcenter/network/WirelessList.qml | 35 +------------------------- utils/Icons.qml | 32 +++++++++++++++-------- 2 files changed, 23 insertions(+), 44 deletions(-) diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 43d56d2..8159291 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -138,44 +138,11 @@ DeviceList { id: icon anchors.centerIn: parent - text: Icons.getNetworkIcon(modelData.strength) + text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure) font.pointSize: Appearance.font.size.large fill: modelData.active ? 1 : 0 color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } - - StyledRect { - id: lockBadge - - visible: modelData.isSecure - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: -Appearance.padding.smaller / 2 - - implicitWidth: lockIconSize + Appearance.padding.smaller - implicitHeight: lockIconSize + Appearance.padding.smaller - radius: Appearance.rounding.full - color: Colours.palette.m3secondaryContainer - - readonly property real lockIconSize: lockIcon.implicitWidth - - Elevation { - anchors.fill: parent - radius: parent.radius - z: -1 - level: 2 - } - - MaterialIcon { - id: lockIcon - - anchors.centerIn: parent - text: "lock" - font.pointSize: Appearance.font.size.small - fill: 1 - color: Colours.palette.m3onSurface - } - } } ColumnLayout { diff --git a/utils/Icons.qml b/utils/Icons.qml index b3fe8ab..b3f00c5 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -116,16 +116,28 @@ Singleton { return fallback; } - function getNetworkIcon(strength: int): string { - if (strength >= 80) - return "signal_wifi_4_bar"; - if (strength >= 60) - return "network_wifi_3_bar"; - if (strength >= 40) - return "network_wifi_2_bar"; - if (strength >= 20) - return "network_wifi_1_bar"; - return "signal_wifi_0_bar"; + function getNetworkIcon(strength: int, isSecure = false): string { + if (isSecure) { + if (strength >= 80) + return "network_wifi_locked"; + if (strength >= 60) + return "network_wifi_3_bar_locked"; + if (strength >= 40) + return "network_wifi_2_bar_locked"; + if (strength >= 20) + return "network_wifi_1_bar_locked"; + return "signal_wifi_0_bar"; + } else { + if (strength >= 80) + return "network_wifi"; + if (strength >= 60) + return "network_wifi_3_bar"; + if (strength >= 40) + return "network_wifi_2_bar"; + if (strength >= 20) + return "network_wifi_1_bar"; + return "signal_wifi_0_bar"; + } } function getBluetoothIcon(icon: string): string { -- cgit v1.2.3-freya From 645159db80ea801ed9a660de40fe3f3321d1f939 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 20 Nov 2025 08:43:58 -0500 Subject: controlcenter: wallpaper title overlay to black --- modules/controlcenter/components/WallpaperGrid.qml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index 2fa4242..7aee181 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -403,27 +403,19 @@ Item { gradient: Gradient { GradientStop { position: 0.0 - color: Qt.rgba(Colours.palette.m3surfaceContainer.r, - Colours.palette.m3surfaceContainer.g, - Colours.palette.m3surfaceContainer.b, 0) + color: Qt.rgba(0, 0, 0, 0) } GradientStop { position: 0.3 - color: Qt.rgba(Colours.palette.m3surfaceContainer.r, - Colours.palette.m3surfaceContainer.g, - Colours.palette.m3surfaceContainer.b, 0.7) + color: Qt.rgba(0, 0, 0, 0.7) } GradientStop { position: 0.6 - color: Qt.rgba(Colours.palette.m3surfaceContainer.r, - Colours.palette.m3surfaceContainer.g, - Colours.palette.m3surfaceContainer.b, 0.9) + color: Qt.rgba(0, 0, 0, 0.9) } GradientStop { position: 1.0 - color: Qt.rgba(Colours.palette.m3surfaceContainer.r, - Colours.palette.m3surfaceContainer.g, - Colours.palette.m3surfaceContainer.b, 0.95) + color: Qt.rgba(0, 0, 0, 0.95) } } -- cgit v1.2.3-freya From a204fb62b50a2ac3d370d6e664e62deb1e7b6ee6 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 20 Nov 2025 08:55:33 -0500 Subject: controlcenter: WallpaperGrid color updates to style --- modules/controlcenter/components/WallpaperGrid.qml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index 7aee181..d1858bc 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -346,6 +346,7 @@ Item { visible: opacity > 0 antialiasing: true smooth: true + sourceSize: Qt.size(width, height) opacity: status === Image.Ready ? 1 : 0 @@ -369,6 +370,7 @@ Item { visible: opacity > 0 antialiasing: true smooth: true + sourceSize: Qt.size(width, height) opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 @@ -403,19 +405,27 @@ Item { gradient: Gradient { GradientStop { position: 0.0 - color: Qt.rgba(0, 0, 0, 0) + color: Qt.rgba(Colours.palette.m3surface.r, + Colours.palette.m3surface.g, + Colours.palette.m3surface.b, 0) } GradientStop { position: 0.3 - color: Qt.rgba(0, 0, 0, 0.7) + color: Qt.rgba(Colours.palette.m3surface.r, + Colours.palette.m3surface.g, + Colours.palette.m3surface.b, 0.7) } GradientStop { position: 0.6 - color: Qt.rgba(0, 0, 0, 0.9) + color: Qt.rgba(Colours.palette.m3surface.r, + Colours.palette.m3surface.g, + Colours.palette.m3surface.b, 0.9) } GradientStop { position: 1.0 - color: Qt.rgba(0, 0, 0, 0.95) + color: Qt.rgba(Colours.palette.m3surface.r, + Colours.palette.m3surface.g, + Colours.palette.m3surface.b, 0.95) } } -- cgit v1.2.3-freya From b8a3af14d7d9c3e37f4d721a2e2ab1fecccfc1bf Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 20 Nov 2025 09:30:45 -0500 Subject: background: enable asynchronous loading for background component --- modules/background/Background.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 3e6d933..fbacfab 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -9,6 +9,7 @@ import Quickshell.Wayland import QtQuick Loader { + asynchronous: true active: Config.background.enabled sourceComponent: Variants { -- cgit v1.2.3-freya From 0029f2d48a67f838f1ca77055625a6dbfd88686e Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 20 Nov 2025 10:42:48 -0500 Subject: controlcenter: wallpaper now uses gridview, removed custom lazy loading --- .../controlcenter/appearance/AppearancePane.qml | 74 ++--- modules/controlcenter/components/WallpaperGrid.qml | 307 ++------------------- 2 files changed, 44 insertions(+), 337 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index aea0085..b6acbe5 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -79,71 +79,49 @@ Item { Component { id: appearanceRightContentComponent - StyledFlickable { + Item { 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 - - SettingsHeader { - icon: "palette" - title: qsTr("Appearance Settings") - } + anchors.fill: parent + spacing: 0 StyledText { - Layout.topMargin: Appearance.spacing.large Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: Appearance.spacing.normal 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 { + Loader { + id: wallpaperLoader + Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.large - Layout.preferredHeight: wallpaperLoader.item ? wallpaperLoader.item.layoutPreferredHeight : 0 + Layout.fillHeight: true + Layout.bottomMargin: -Appearance.padding.large * 2 - Loader { - id: wallpaperLoader - anchors.fill: parent - asynchronous: true - active: { - const isActive = root.session.activeIndex === 3; - const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1; - 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!"); - } - } - - sourceComponent: WallpaperGrid { - session: root.session + asynchronous: true + active: { + const isActive = root.session.activeIndex === 3; + const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1; + 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!"); } } + + sourceComponent: WallpaperGrid { + session: root.session + } } } } diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index d1858bc..fa72291 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -10,304 +10,35 @@ import qs.config import Caelestia.Models import QtQuick -Item { +GridView { id: root required property Session session - property alias layoutPreferredHeight: wallpaperGrid.layoutPreferredHeight + readonly property int minCellWidth: 200 + Appearance.spacing.normal + readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth)) - // 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; - } - } - - QtObject { - id: lazyModel - - property var sourceList: null - property int loadedCount: 0 - property int visibleCount: 0 - property int totalCount: 0 - - function initialize(list) { - sourceList = list; - totalCount = list ? list.length : 0; - 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; - } - } + cellWidth: width / columnsCount + cellHeight: 140 + Appearance.spacing.normal - function updateVisibleCount(neededCount) { - 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); + model: Wallpapers.list - if (newVisibleCount > visibleCount) { - visibleCount = newVisibleCount; - } - } + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: root } - 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)) - - 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 - - ListModel { - id: wallpaperListModel - } - - model: wallpaperListModel - - Connections { - target: lazyModel - function onVisibleCountChanged(): void { - if (!lazyModel || !lazyModel.sourceList) return; - - const newCount = lazyModel.visibleCount; - const currentCount = wallpaperListModel.count; - - if (newCount > currentCount) { - const flickable = root.parentFlickable; - const oldScrollY = flickable ? flickable.contentY : 0; - - for (let i = currentCount; i < newCount; i++) { - wallpaperListModel.append({modelData: lazyModel.sourceList[i]}); - } - - 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: root.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 = root.parentFlickable; - if (!flickable) return; - - const gridY = root.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); - - 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 = root.parentFlickable; - if (!flickable || !lazyModel || !lazyModel.sourceList) return; - - const gridY = root.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); + delegate: Item { + required property var modelData + required property int index - const loadedRows = Math.ceil(lazyModel.loadedCount / wallpaperGrid.columnsCount); - const rowsRemaining = loadedRows - (bottomRow + 1); + width: root.cellWidth + height: root.cellHeight - if (rowsRemaining <= 1 && lazyModel.loadedCount < lazyModel.totalCount) { - if (!wallpaperGrid._expansionInProgress) { - wallpaperGrid._expansionInProgress = true; - lazyModel.loadOneRow(); - Qt.callLater(function() { - wallpaperGrid._expansionInProgress = false; - }); - } - } - } - } - - 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++; - } + readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent + readonly property real itemMargin: Appearance.spacing.normal / 2 + readonly property real itemRadius: Appearance.rounding.normal StateLayer { anchors.fill: parent @@ -514,5 +245,3 @@ Item { } } } -} - -- cgit v1.2.3-freya From a2b00d4a0ea90b84e60159140cb56355cddc3058 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 20 Nov 2025 13:30:38 -0500 Subject: controlcenter: corrected background transparency on tooltips --- components/controls/Tooltip.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index bab8086..d665083 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -45,6 +45,7 @@ Popup { closePolicy: Popup.NoAutoClose padding: 0 margins: 0 + background: Item {} // Update position when target moves or tooltip becomes visible onTooltipVisibleChanged: { @@ -141,6 +142,7 @@ Popup { color: Colours.palette.m3surfaceContainerHighest radius: Appearance.rounding.small + antialiasing: true // Add elevation for depth Elevation { -- cgit v1.2.3-freya From 284e7b42971928465f11a83c4ccb8e95817183e2 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 20 Nov 2025 20:19:49 -0500 Subject: controlcenter: corrected taskbar panel container margins/padding --- modules/controlcenter/taskbar/TaskbarPane.qml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 38c1179..5d51c8c 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -112,7 +112,7 @@ Item { anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 + anchors.rightMargin: Appearance.padding.normal radius: taskbarBorder.innerRadius color: "transparent" @@ -123,7 +123,7 @@ Item { 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 + anchors.rightMargin: Appearance.padding.large asynchronous: true sourceComponent: taskbarContentComponent @@ -133,7 +133,7 @@ Item { InnerBorder { id: taskbarBorder leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 + rightThickness: Appearance.padding.normal } Component { @@ -240,10 +240,12 @@ Item { } RowLayout { + id: mainRowLayout Layout.fillWidth: true spacing: Appearance.spacing.normal ColumnLayout { + id: leftColumnLayout Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal @@ -468,6 +470,7 @@ Item { } ColumnLayout { + id: middleColumnLayout Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal @@ -543,6 +546,7 @@ Item { } ColumnLayout { + id: rightColumnLayout Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal -- cgit v1.2.3-freya From 7779de55bbcc87ad4af7bcc4b0f4da6e0fe65847 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 20 Nov 2025 21:58:55 -0500 Subject: controlcenter: refactor SliderInput and StyledInputFields to use qt components and consolidated SliderInputs to single component --- components/controls/StyledInputField.qml | 80 +++++++++ modules/controlcenter/audio/AudioPane.qml | 158 ++++++------------ modules/controlcenter/components/SliderInput.qml | 106 +++++------- .../components/controls/SliderInput.qml | 179 --------------------- 4 files changed, 172 insertions(+), 351 deletions(-) create mode 100644 components/controls/StyledInputField.qml delete mode 100644 modules/controlcenter/components/controls/SliderInput.qml diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml new file mode 100644 index 0000000..fcd0a33 --- /dev/null +++ b/components/controls/StyledInputField.qml @@ -0,0 +1,80 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.services +import qs.config +import QtQuick + +Item { + id: root + + property string text: "" + property var validator: null + property bool readOnly: false + property int horizontalAlignment: TextInput.AlignHCenter + property int implicitWidth: 70 + property bool enabled: true + + // Expose activeFocus through alias to avoid FINAL property override + readonly property alias hasFocus: inputField.activeFocus + + signal textEdited(string text) + signal editingFinished() + + implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 + + StyledRect { + id: container + + anchors.fill: parent + color: inputHover.containsMouse || inputField.activeFocus + ? Colours.layer(Colours.palette.m3surfaceContainer, 3) + : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: inputField.activeFocus + ? Colours.palette.m3primary + : Qt.alpha(Colours.palette.m3outline, 0.3) + opacity: root.enabled ? 1 : 0.5 + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + MouseArea { + id: inputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + enabled: root.enabled + } + + StyledTextField { + id: inputField + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: root.horizontalAlignment + validator: root.validator + readOnly: root.readOnly + enabled: root.enabled + + Binding { + target: inputField + property: "text" + value: root.text + when: !inputField.activeFocus + } + + onTextChanged: { + root.text = text; + root.textEdited(text); + } + + onEditingFinished: { + root.editingFinished(); + } + } + } +} + diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 694e178..76122f9 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -263,67 +263,40 @@ Item { Layout.fillWidth: true } - StyledRect { + StyledInputField { + id: outputVolumeInput 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) + validator: IntValidator { bottom: 0; top: 100 } 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 + + Component.onCompleted: { + 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(); - } - - Connections { - target: Audio - function onVolumeChanged() { - if (!outputVolumeInput.activeFocus) { - outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); - } + + Connections { + target: Audio + function onVolumeChanged() { + if (!outputVolumeInput.hasFocus) { + 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: { + } + + onTextEdited: (text) => { + if (hasFocus) { const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(Audio.volume * 100).toString(); + 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 { @@ -368,7 +341,7 @@ Item { opacity: enabled ? 1 : 0.5 onMoved: { Audio.setVolume(value); - if (!outputVolumeInput.activeFocus) { + if (!outputVolumeInput.hasFocus) { outputVolumeInput.text = Math.round(value * 100).toString(); } } @@ -402,67 +375,40 @@ Item { Layout.fillWidth: true } - StyledRect { + StyledInputField { + id: inputVolumeInput 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) + validator: IntValidator { bottom: 0; top: 100 } 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 + + Component.onCompleted: { + 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(); - } - - Connections { - target: Audio - function onSourceVolumeChanged() { - if (!inputVolumeInput.activeFocus) { - inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); - } + + Connections { + target: Audio + function onSourceVolumeChanged() { + if (!inputVolumeInput.hasFocus) { + 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: { + } + + onTextEdited: (text) => { + if (hasFocus) { const val = parseInt(text); - if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(Audio.sourceVolume * 100).toString(); + 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 { @@ -507,7 +453,7 @@ Item { opacity: enabled ? 1 : 0.5 onMoved: { Audio.setSourceVolume(value); - if (!inputVolumeInput.activeFocus) { + if (!inputVolumeInput.hasFocus) { inputVolumeInput.text = Math.round(value * 100).toString(); } } diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index 3d7cd4d..7348368 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -76,79 +76,53 @@ ColumnLayout { Layout.fillWidth: true } - StyledRect { + StyledInputField { + id: inputField Layout.preferredWidth: 70 - implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 - color: inputHover.containsMouse || inputField.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: inputField.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: inputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton + validator: root.validator + + Component.onCompleted: { + // Initialize text without triggering valueModified signal + text = root.formatValue(root.value); } - - StyledTextField { - id: inputField - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: root.validator - - Component.onCompleted: { - // Initialize text without triggering valueModified signal - text = root.formatValue(root.value); - } - - onTextChanged: { - if (activeFocus) { - const val = root.parseValue(text); - if (!isNaN(val)) { - // Validate against validator bounds if available - let isValid = true; - if (root.validator) { - if (root.validator.bottom !== undefined && val < root.validator.bottom) { - isValid = false; - } - if (root.validator.top !== undefined && val > root.validator.top) { - isValid = false; - } + + onTextEdited: (text) => { + if (hasFocus) { + const val = root.parseValue(text); + if (!isNaN(val)) { + // Validate against validator bounds if available + let isValid = true; + if (root.validator) { + if (root.validator.bottom !== undefined && val < root.validator.bottom) { + isValid = false; } - - if (isValid) { - root.valueModified(val); + if (root.validator.top !== undefined && val > root.validator.top) { + isValid = false; } } + + if (isValid) { + root.valueModified(val); + } } } - - onEditingFinished: { - const val = root.parseValue(text); - let isValid = true; - if (root.validator) { - if (root.validator.bottom !== undefined && val < root.validator.bottom) { - isValid = false; - } - if (root.validator.top !== undefined && val > root.validator.top) { - isValid = false; - } + } + + onEditingFinished: { + const val = root.parseValue(text); + let isValid = true; + if (root.validator) { + if (root.validator.bottom !== undefined && val < root.validator.bottom) { + isValid = false; } - - if (isNaN(val) || !isValid) { - text = root.formatValue(root.value); + if (root.validator.top !== undefined && val > root.validator.top) { + isValid = false; } } + + if (isNaN(val) || !isValid) { + text = root.formatValue(root.value); + } } } @@ -181,7 +155,7 @@ ColumnLayout { onValueChanged: { // Update input field text in real-time as slider moves during dragging // Always update when slider value changes (during dragging or external updates) - if (!inputField.activeFocus) { + if (!inputField.hasFocus) { const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value; inputField.text = root.formatValue(newValue); } @@ -190,7 +164,7 @@ ColumnLayout { onMoved: { const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value; root.valueModified(newValue); - if (!inputField.activeFocus) { + if (!inputField.hasFocus) { inputField.text = root.formatValue(newValue); } } @@ -199,7 +173,7 @@ ColumnLayout { // Update input field when value changes externally (slider is already bound) onValueChanged: { // Only update if component is initialized to avoid issues during creation - if (root._initialized && !inputField.activeFocus) { + if (root._initialized && !inputField.hasFocus) { inputField.text = root.formatValue(root.value); } } diff --git a/modules/controlcenter/components/controls/SliderInput.qml b/modules/controlcenter/components/controls/SliderInput.qml deleted file mode 100644 index a114f7f..0000000 --- a/modules/controlcenter/components/controls/SliderInput.qml +++ /dev/null @@ -1,179 +0,0 @@ -pragma ComponentBehavior: Bound - -import ".." -import qs.components -import qs.components.controls -import qs.components.effects -import QtQuick -import QtQuick.Layouts - -ColumnLayout { - id: root - - property string label: "" - property real value: 0 - property real from: 0 - property real to: 100 - property real stepSize: 0 - property var validator: null - property string suffix: "" // Optional suffix text (e.g., "×", "px") - property var formatValueFunction: null // Optional custom format function - property var parseValueFunction: null // Optional custom parse function - - function formatValue(val: real): string { - if (formatValueFunction) { - return formatValueFunction(val); - } - // Default format function - if (validator && validator.bottom !== undefined) { - // Check if it's an integer validator - if (validator.top !== undefined && validator.top === Math.floor(validator.top)) { - return Math.round(val).toString(); - } - } - return val.toFixed(1); - } - - function parseValue(text: string): real { - if (parseValueFunction) { - return parseValueFunction(text); - } - // Default parse function - if (validator && validator.bottom !== undefined) { - // Check if it's an integer validator - if (validator.top !== undefined && validator.top === Math.floor(validator.top)) { - return parseInt(text); - } - } - return parseFloat(text); - } - - signal valueChanged(real newValue) - - spacing: Appearance.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - StyledText { - visible: root.label !== "" - text: root.label - font.pointSize: Appearance.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - StyledRect { - Layout.preferredWidth: 70 - implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 - color: inputHover.containsMouse || inputField.activeFocus - ? Colours.layer(Colours.palette.m3surfaceContainer, 3) - : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small - border.width: 1 - border.color: inputField.activeFocus - ? Colours.palette.m3primary - : Qt.alpha(Colours.palette.m3outline, 0.3) - - Behavior on color { CAnim {} } - Behavior on border.color { CAnim {} } - - MouseArea { - id: inputHover - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - acceptedButtons: Qt.NoButton - } - - StyledTextField { - id: inputField - anchors.centerIn: parent - width: parent.width - Appearance.padding.normal - horizontalAlignment: TextInput.AlignHCenter - validator: root.validator - - Component.onCompleted: { - text = root.formatValue(root.value); - } - - onTextChanged: { - if (activeFocus) { - const val = root.parseValue(text); - if (!isNaN(val)) { - // Validate against validator bounds if available - let isValid = true; - if (root.validator) { - if (root.validator.bottom !== undefined && val < root.validator.bottom) { - isValid = false; - } - if (root.validator.top !== undefined && val > root.validator.top) { - isValid = false; - } - } - - if (isValid) { - root.valueChanged(val); - } - } - } - } - - onEditingFinished: { - const val = root.parseValue(text); - let isValid = true; - if (root.validator) { - if (root.validator.bottom !== undefined && val < root.validator.bottom) { - isValid = false; - } - if (root.validator.top !== undefined && val > root.validator.top) { - isValid = false; - } - } - - if (isNaN(val) || !isValid) { - text = root.formatValue(root.value); - } - } - } - } - - StyledText { - visible: root.suffix !== "" - text: root.suffix - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - } - } - - StyledSlider { - id: slider - - Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 - - from: root.from - to: root.to - stepSize: root.stepSize - value: root.value - - onMoved: { - const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value; - root.valueChanged(newValue); - if (!inputField.activeFocus) { - inputField.text = root.formatValue(newValue); - } - } - } - - // Update input field when value changes externally (slider is already bound) - onValueChanged: { - if (!inputField.activeFocus) { - inputField.text = root.formatValue(root.value); - } - } -} - -- cgit v1.2.3-freya From 8e9be711f1218d8322d4f5e1800377d15463fe35 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 20 Nov 2025 22:34:20 -0500 Subject: controlcenter: cleaned up wallpapergrid --- modules/controlcenter/components/WallpaperGrid.qml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index fa72291..5eab5b8 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -216,13 +216,7 @@ GridView { 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 + text: modelData.name font.pointSize: Appearance.font.size.smaller font.weight: 500 color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface -- cgit v1.2.3-freya