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 --- modules/bar/Bar.qml | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'modules/bar') 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(-) (limited to 'modules/bar') 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 (limited to 'modules/bar') 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 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(-) (limited to 'modules/bar') 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 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 (limited to 'modules/bar') 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(-) (limited to 'modules/bar') 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(-) (limited to 'modules/bar') 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 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(-) (limited to 'modules/bar') 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 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(-) (limited to 'modules/bar') 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 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(-) (limited to 'modules/bar') 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 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(-) (limited to 'modules/bar') 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(-) (limited to 'modules/bar') 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 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 (limited to 'modules/bar') 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 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 (limited to 'modules/bar') 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(-) (limited to 'modules/bar') 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(-) (limited to 'modules/bar') 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 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 (limited to 'modules/bar') 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 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(-) (limited to 'modules/bar') 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(-) (limited to 'modules/bar') 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(-) (limited to 'modules/bar') 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(-) (limited to 'modules/bar') 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 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 (limited to 'modules/bar') 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(-) (limited to 'modules/bar') 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 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(-) (limited to 'modules/bar') 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 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(-) (limited to 'modules/bar') 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 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 (limited to 'modules/bar') 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(-) (limited to 'modules/bar') 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 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 (limited to 'modules/bar') 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