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 --- modules/bar/popouts/Content.qml | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'modules/bar/popouts/Content.qml') 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 { -- 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/popouts/Content.qml') diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 6113539..1afa616 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -147,15 +147,15 @@ StyledRect { sourceComponent: MaterialIcon { animate: true - text: Network.active ? Icons.getNetworkIcon(Network.active.strength ?? 0) : "wifi_off" + text: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off" color: root.colour } } // Ethernet icon WrappedLoader { - name: "ethernet" - active: Config.bar.status.showEthernet && Network.activeEthernet + name: "network" + active: Config.bar.status.showEthernet && Nmcli.activeEthernet sourceComponent: MaterialIcon { animate: true diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index f8ef549..e3f569d 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -36,11 +36,6 @@ Item { sourceComponent: Network {} } - Popout { - name: "ethernet" - sourceComponent: Ethernet {} - } - Popout { name: "bluetooth" sourceComponent: Bluetooth { diff --git a/modules/bar/popouts/Ethernet.qml b/modules/bar/popouts/Ethernet.qml deleted file mode 100644 index b4a753c..0000000 --- a/modules/bar/popouts/Ethernet.qml +++ /dev/null @@ -1,179 +0,0 @@ -pragma ComponentBehavior: Bound - -import qs.components -import qs.components.controls -import qs.services -import qs.config -import qs.utils -import Quickshell -import QtQuick -import QtQuick.Layouts - -ColumnLayout { - id: root - - spacing: Appearance.spacing.small - width: Config.bar.sizes.networkWidth - - StyledText { - Layout.topMargin: Appearance.padding.normal - Layout.rightMargin: Appearance.padding.small - text: qsTr("Ethernet") - font.weight: 500 - } - - StyledText { - Layout.topMargin: Appearance.spacing.small - Layout.rightMargin: Appearance.padding.small - text: qsTr("%1 devices available").arg(Network.ethernetDeviceCount || Network.ethernetDevices.length) - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small - } - - Repeater { - model: ScriptModel { - values: [...Network.ethernetDevices].sort((a, b) => { - if (a.connected !== b.connected) - return b.connected - a.connected; - return (a.interface || "").localeCompare(b.interface || ""); - }).slice(0, 8) - } - - RowLayout { - id: ethernetItem - - required property var modelData - readonly property bool loading: false - - Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - spacing: Appearance.spacing.small - - opacity: 0 - scale: 0.7 - - Component.onCompleted: { - opacity = 1; - scale = 1; - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim {} - } - - MaterialIcon { - text: "cable" - color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - } - - StyledText { - Layout.leftMargin: Appearance.spacing.small / 2 - Layout.rightMargin: Appearance.spacing.small / 2 - Layout.fillWidth: true - text: ethernetItem.modelData.interface || qsTr("Unknown") - elide: Text.ElideRight - font.weight: ethernetItem.modelData.connected ? 500 : 400 - color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface - } - - StyledRect { - id: connectBtn - - implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.small - - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0) - - CircularIndicator { - anchors.fill: parent - running: ethernetItem.loading - } - - StateLayer { - color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: ethernetItem.loading - - function onClicked(): void { - if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { - Network.disconnectEthernet(ethernetItem.modelData.connection); - } else { - Network.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || ""); - } - } - } - - MaterialIcon { - id: connectIcon - - anchors.centerIn: parent - animate: true - text: ethernetItem.modelData.connected ? "link_off" : "link" - color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - - opacity: ethernetItem.loading ? 0 : 1 - - Behavior on opacity { - Anim {} - } - } - } - } - } - - StyledRect { - Layout.topMargin: Appearance.spacing.small - Layout.fillWidth: true - implicitHeight: refreshBtn.implicitHeight + Appearance.padding.small * 2 - - radius: Appearance.rounding.full - color: Colours.palette.m3primaryContainer - - StateLayer { - color: Colours.palette.m3onPrimaryContainer - disabled: Network.ethernetProcessRunning - - function onClicked(): void { - Network.getEthernetDevices(); - } - } - - RowLayout { - id: refreshBtn - - anchors.centerIn: parent - spacing: Appearance.spacing.small - opacity: Network.ethernetProcessRunning ? 0 : 1 - - MaterialIcon { - id: refreshIcon - - animate: true - text: "refresh" - color: Colours.palette.m3onPrimaryContainer - } - - StyledText { - text: qsTr("Refresh devices") - color: Colours.palette.m3onPrimaryContainer - } - - Behavior on opacity { - Anim {} - } - } - - CircularIndicator { - anchors.centerIn: parent - strokeWidth: Appearance.padding.small / 2 - bgColour: "transparent" - implicitHeight: parent.implicitHeight - Appearance.padding.smaller * 2 - running: Network.ethernetProcessRunning - } - } -} - diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index cb012bf..d185887 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -192,6 +192,116 @@ ColumnLayout { } } + StyledText { + Layout.topMargin: Appearance.spacing.normal + Layout.rightMargin: Appearance.padding.small + text: qsTr("Ethernet") + font.weight: 500 + } + + StyledText { + Layout.topMargin: Appearance.spacing.small + Layout.rightMargin: Appearance.padding.small + text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length) + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + } + + Repeater { + model: ScriptModel { + values: [...Nmcli.ethernetDevices].sort((a, b) => { + if (a.connected !== b.connected) + return b.connected - a.connected; + return (a.interface || "").localeCompare(b.interface || ""); + }).slice(0, 8) + } + + RowLayout { + id: ethernetItem + + required property var modelData + readonly property bool loading: false + + Layout.fillWidth: true + Layout.rightMargin: Appearance.padding.small + spacing: Appearance.spacing.small + + opacity: 0 + scale: 0.7 + + Component.onCompleted: { + opacity = 1; + scale = 1; + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + MaterialIcon { + text: "cable" + color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + } + + StyledText { + Layout.leftMargin: Appearance.spacing.small / 2 + Layout.rightMargin: Appearance.spacing.small / 2 + Layout.fillWidth: true + text: ethernetItem.modelData.interface || qsTr("Unknown") + elide: Text.ElideRight + font.weight: ethernetItem.modelData.connected ? 500 : 400 + color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface + } + + StyledRect { + id: connectBtn + + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.small + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0) + + CircularIndicator { + anchors.fill: parent + running: ethernetItem.loading + } + + StateLayer { + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: ethernetItem.loading + + function onClicked(): void { + if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { + Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); + } else { + Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {}); + } + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + animate: true + text: ethernetItem.modelData.connected ? "link_off" : "link" + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + + opacity: ethernetItem.loading ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + } + } + } + Connections { target: Nmcli -- cgit v1.2.3-freya From 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/popouts/Content.qml') 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/popouts/Content.qml') diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 661a41c..7561eec 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -32,8 +32,10 @@ Item { } Popout { + id: networkPopout name: "network" sourceComponent: Network { + wrapper: root.wrapper view: "wireless" } } @@ -41,10 +43,56 @@ Item { Popout { name: "ethernet" sourceComponent: Network { + wrapper: root.wrapper view: "ethernet" } } + Popout { + id: passwordPopout + name: "wirelesspassword" + sourceComponent: WirelessPasswordPopout { + id: passwordComponent + wrapper: root.wrapper + network: networkPopout.item?.passwordNetwork ?? null + } + + Connections { + target: root.wrapper + function onCurrentNameChanged() { + // Update network immediately when password popout becomes active + if (root.wrapper.currentName === "wirelesspassword") { + // Set network immediately if available + if (networkPopout.item && networkPopout.item.passwordNetwork) { + if (passwordPopout.item) { + passwordPopout.item.network = networkPopout.item.passwordNetwork; + } + } + // Also try after a short delay in case networkPopout.item wasn't ready + Qt.callLater(() => { + if (passwordPopout.item && networkPopout.item && networkPopout.item.passwordNetwork) { + passwordPopout.item.network = networkPopout.item.passwordNetwork; + } + }, 100); + } + } + } + + Connections { + target: networkPopout + function onItemChanged() { + // When network popout loads, update password popout if it's active + if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) { + Qt.callLater(() => { + if (networkPopout.item && networkPopout.item.passwordNetwork) { + passwordPopout.item.network = networkPopout.item.passwordNetwork; + } + }); + } + } + } + } + Popout { name: "bluetooth" sourceComponent: Bluetooth { diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index b807ce4..93ff867 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -12,8 +12,12 @@ import QtQuick.Layouts ColumnLayout { id: root + required property Item wrapper + property string connectingToSsid: "" property string view: "wireless" // "wireless" or "ethernet" + property var passwordNetwork: null + property bool showPasswordDialog: false spacing: Appearance.spacing.small width: Config.bar.sizes.networkWidth @@ -129,7 +133,27 @@ ColumnLayout { Nmcli.disconnectFromNetwork(); } else { root.connectingToSsid = networkItem.modelData.ssid; - Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); + // Check if network is secure + if (networkItem.modelData.isSecure) { + // Try to connect first - will show password dialog if password is needed + Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, (result) => { + if (result && result.needsPassword) { + // Password is required - show password dialog + root.passwordNetwork = networkItem.modelData; + root.showPasswordDialog = true; + root.wrapper.currentName = "wirelesspassword"; + } else if (result && result.success) { + // Connection successful with saved password + root.connectingToSsid = ""; + } else { + // Connection failed for other reasons + root.connectingToSsid = ""; + } + }); + } else { + // Open network, no password needed + Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); + } } } } @@ -329,6 +353,15 @@ ColumnLayout { function onActiveChanged(): void { if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { root.connectingToSsid = ""; + // Close password dialog if we successfully connected + if (root.showPasswordDialog && root.passwordNetwork && + Nmcli.active.ssid === root.passwordNetwork.ssid) { + root.showPasswordDialog = false; + root.passwordNetwork = null; + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + } } } @@ -338,6 +371,17 @@ ColumnLayout { } } + Connections { + target: root.wrapper + function onCurrentNameChanged(): void { + // Clear password network when leaving password dialog + if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) { + root.showPasswordDialog = false; + root.passwordNetwork = null; + } + } + } + component Toggle: RowLayout { required property string label property alias checked: toggle.checked diff --git a/modules/bar/popouts/WirelessPasswordPopout.qml b/modules/bar/popouts/WirelessPasswordPopout.qml new file mode 100644 index 0000000..80bdd10 --- /dev/null +++ b/modules/bar/popouts/WirelessPasswordPopout.qml @@ -0,0 +1,599 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.controls +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Item wrapper + property var network: null + property bool isClosing: false + + readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" + + Connections { + target: root.wrapper + function onCurrentNameChanged() { + if (root.wrapper.currentName === "wirelesspassword") { + // Update network when popout becomes active + Qt.callLater(() => { + // Try to get network from parent Content's networkPopout + const content = root.parent?.parent?.parent; + if (content) { + const networkPopout = content.children.find(c => c.name === "network"); + if (networkPopout && networkPopout.item) { + root.network = networkPopout.item.passwordNetwork; + } + } + // Force focus to password container when popout becomes active + Qt.callLater(() => { + passwordContainer.forceActiveFocus(); + }, 100); + }, 100); + } + } + } + + spacing: Appearance.spacing.normal + + implicitWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + + visible: shouldBeVisible || isClosing + enabled: shouldBeVisible && !isClosing + focus: enabled + + Component.onCompleted: { + if (shouldBeVisible) { + Qt.callLater(() => { + root.forceActiveFocus(); + passwordContainer.forceActiveFocus(); + }, 150); + } + } + + onShouldBeVisibleChanged: { + if (shouldBeVisible) { + Qt.callLater(() => { + root.forceActiveFocus(); + passwordContainer.forceActiveFocus(); + }, 150); + } + } + + Keys.onEscapePressed: { + closeDialog(); + } + + StyledRect { + Layout.fillWidth: true + Layout.preferredWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surface + opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 + scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + ParallelAnimation { + running: root.isClosing + onFinished: { + if (root.isClosing) { + root.isClosing = false; + } + } + + Anim { + target: parent + property: "opacity" + to: 0 + } + Anim { + target: parent + property: "scale" + to: 0.7 + } + } + + Keys.onEscapePressed: closeDialog(); + + ColumnLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "lock" + font.pointSize: Appearance.font.size.extraLarge * 2 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enter password") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + id: networkNameText + Layout.alignment: Qt.AlignHCenter + text: { + if (root.network) { + const ssid = root.network.ssid; + if (ssid && ssid.length > 0) { + return qsTr("Network: %1").arg(ssid); + } + } + return qsTr("Network: Unknown"); + } + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + Timer { + interval: 50 + running: root.shouldBeVisible && (!root.network || !root.network.ssid) + repeat: true + property int attempts: 0 + onTriggered: { + attempts++; + // Keep trying to get network from Network component + const content = root.parent?.parent?.parent; + if (content) { + const networkPopout = content.children.find(c => c.name === "network"); + if (networkPopout && networkPopout.item && networkPopout.item.passwordNetwork) { + root.network = networkPopout.item.passwordNetwork; + } + } + // Stop if we got it or after 20 attempts (1 second) + if ((root.network && root.network.ssid) || attempts >= 20) { + stop(); + attempts = 0; + } + } + onRunningChanged: { + if (!running) { + attempts = 0; + } + } + } + + StyledText { + id: statusText + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.spacing.small + visible: connectButton.connecting + text: { + if (connectButton.connecting) { + return qsTr("Connecting..."); + } + return ""; + } + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + font.weight: 400 + wrapMode: Text.WordWrap + Layout.maximumWidth: parent.width - Appearance.padding.large * 2 + } + + FocusScope { + id: passwordContainer + objectName: "passwordContainer" + Layout.topMargin: Appearance.spacing.large + Layout.fillWidth: true + implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + + focus: true + activeFocusOnTab: true + Keys.onPressed: event => { + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { + if (connectButton.enabled) { + connectButton.clicked(); + } + event.accepted = true; + } else if (event.key === Qt.Key_Backspace) { + if (event.modifiers & Qt.ControlModifier) { + passwordBuffer = ""; + } else { + passwordBuffer = passwordBuffer.slice(0, -1); + } + event.accepted = true; + } else if (event.text && event.text.length > 0) { + passwordBuffer += event.text; + event.accepted = true; + } + } + + property string passwordBuffer: "" + + Connections { + target: root + function onShouldBeVisibleChanged(): void { + if (root.shouldBeVisible) { + Qt.callLater(() => { + passwordContainer.forceActiveFocus(); + }, 50); + passwordContainer.passwordBuffer = ""; + } + } + } + + Component.onCompleted: { + if (root.shouldBeVisible) { + Qt.callLater(() => { + passwordContainer.forceActiveFocus(); + }, 100); + } + } + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + border.width: passwordContainer.activeFocus ? 2 : 1 + border.color: passwordContainer.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline + + Behavior on border.color { + CAnim {} + } + } + + StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor + + function onClicked(): void { + passwordContainer.forceActiveFocus(); + } + } + + StyledText { + id: placeholder + + anchors.centerIn: parent + text: qsTr("Password") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + font.family: Appearance.font.family.mono + opacity: passwordContainer.passwordBuffer ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + + ListView { + id: charList + + readonly property int fullWidth: count * (implicitHeight + spacing) - spacing + + anchors.centerIn: parent + implicitWidth: fullWidth + implicitHeight: Appearance.font.size.normal + + orientation: Qt.Horizontal + spacing: Appearance.spacing.small / 2 + interactive: false + + model: ScriptModel { + values: passwordContainer.passwordBuffer.split("") + } + + delegate: StyledRect { + id: ch + + implicitWidth: implicitHeight + implicitHeight: charList.implicitHeight + + color: Colours.palette.m3onSurface + radius: Appearance.rounding.small / 2 + + opacity: 0 + scale: 0 + Component.onCompleted: { + opacity = 1; + scale = 1; + } + ListView.onRemove: removeAnim.start() + + SequentialAnimation { + id: removeAnim + + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: true + } + ParallelAnimation { + Anim { + target: ch + property: "opacity" + to: 0 + } + Anim { + target: ch + property: "scale" + to: 0.5 + } + } + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: false + } + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } + + Behavior on implicitWidth { + Anim {} + } + } + } + + RowLayout { + Layout.topMargin: Appearance.spacing.normal + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + SimpleButton { + id: cancelButton + + Layout.fillWidth: true + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + color: Colours.palette.m3secondaryContainer + onColor: Colours.palette.m3onSecondaryContainer + text: qsTr("Cancel") + + onClicked: closeDialog(); + } + + SimpleButton { + id: connectButton + + Layout.fillWidth: true + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + color: Colours.palette.m3primary + onColor: Colours.palette.m3onPrimary + text: qsTr("Connect") + enabled: passwordContainer.passwordBuffer.length > 0 && !connecting + + property bool connecting: false + + onClicked: { + if (!root.network || connecting) { + return; + } + + const password = passwordContainer.passwordBuffer; + if (!password || password.length === 0) { + return; + } + + // Set connecting state + connecting = true; + enabled = false; + text = qsTr("Connecting..."); + + // Connect to network + Nmcli.connectToNetwork( + root.network.ssid, + password, + root.network.bssid || "", + (result) => { + if (result && result.success) { + // Connection successful, monitor will handle the rest + } else if (result && result.needsPassword) { + // Shouldn't happen since we provided password + connectionMonitor.stop(); + connecting = false; + enabled = true; + text = qsTr("Connect"); + } else { + // Connection failed immediately - return to network popout + connectionMonitor.stop(); + connecting = false; + enabled = true; + text = qsTr("Connect"); + Qt.callLater(() => { + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + }, 500); + } + } + ); + + // Start monitoring connection + connectionMonitor.start(); + } + } + } + } + } + + function checkConnectionStatus(): void { + if (!root.shouldBeVisible || !connectButton.connecting) { + return; + } + + // Check if we're connected to the target network (case-insensitive SSID comparison) + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && + Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + + if (isConnected) { + // Successfully connected - give it a moment for network list to update + Qt.callLater(() => { + // Double-check connection is still active + if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { + const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + // Return to network popout on successful connection + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + closeDialog(); + } + } + }, 500); + return; + } + + // Check for connection failures - if pending connection was cleared but we're not connected + if (Nmcli.pendingConnection === null && connectButton.connecting) { + // Wait a bit more before giving up (allow time for connection to establish) + if (connectionMonitor.repeatCount > 10) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + // Return to network popout on timeout failure + Qt.callLater(() => { + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + }, 500); + } + } + } + + Timer { + id: connectionMonitor + interval: 1000 + repeat: true + triggeredOnStart: false + property int repeatCount: 0 + + onTriggered: { + repeatCount++; + checkConnectionStatus(); + } + + onRunningChanged: { + if (!running) { + repeatCount = 0; + } + } + } + + Connections { + target: Nmcli + function onActiveChanged() { + if (root.shouldBeVisible) { + checkConnectionStatus(); + } + } + function onConnectionFailed(ssid: string) { + if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + // Return to network popout on connection failure + Qt.callLater(() => { + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + }, 500); + } + } + } + + function closeDialog(): void { + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); + + // Return to network popout + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + } + + component SimpleButton: StyledRect { + id: button + + property color onColor: Colours.palette.m3onSurface + property alias disabled: stateLayer.disabled + property alias text: label.text + property alias enabled: stateLayer.enabled + property string icon: "" + + implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: rowLayout.implicitHeight + Appearance.padding.small + radius: Appearance.rounding.normal + + StateLayer { + id: stateLayer + color: parent.onColor + function onClicked(): void { + if (parent.enabled !== false) { + parent.clicked(); + } + } + } + + RowLayout { + id: rowLayout + anchors.centerIn: parent + spacing: Appearance.spacing.small + + MaterialIcon { + id: iconItem + visible: button.icon.length > 0 + text: button.icon + color: button.onColor + font.pointSize: Appearance.font.size.large + } + + StyledText { + id: label + Layout.leftMargin: button.icon.length > 0 ? Appearance.padding.smaller : 0 + color: parent.parent.onColor + } + } + + signal clicked + } +} + diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 4be47e4..5ef4f9d 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -55,7 +55,25 @@ Item { implicitWidth: nonAnimWidth implicitHeight: nonAnimHeight - Keys.onEscapePressed: close() + focus: hasCurrent + Keys.onEscapePressed: { + // Forward escape to password popout if active, otherwise close + if (currentName === "wirelesspassword" && content.item) { + const passwordPopout = content.item.children.find(c => c.name === "wirelesspassword"); + if (passwordPopout && passwordPopout.item) { + passwordPopout.item.closeDialog(); + return; + } + } + close(); + } + + Keys.onPressed: event => { + // Don't intercept keys when password popout is active - let it handle them + if (currentName === "wirelesspassword") { + event.accepted = false; + } + } HyprlandFocusGrab { active: root.isDetached @@ -70,6 +88,14 @@ Item { property: "WlrLayershell.keyboardFocus" value: WlrKeyboardFocus.OnDemand } + + Binding { + when: root.hasCurrent && root.currentName === "wirelesspassword" + + target: QsWindow.window + property: "WlrLayershell.keyboardFocus" + value: WlrKeyboardFocus.OnDemand + } Comp { id: content -- cgit v1.2.3-freya From 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/popouts/Content.qml') diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 7561eec..da993fa 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -51,7 +51,7 @@ Item { Popout { id: passwordPopout name: "wirelesspassword" - sourceComponent: WirelessPasswordPopout { + sourceComponent: WirelessPassword { id: passwordComponent wrapper: root.wrapper network: networkPopout.item?.passwordNetwork ?? null diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml new file mode 100644 index 0000000..a9d098e --- /dev/null +++ b/modules/bar/popouts/WirelessPassword.qml @@ -0,0 +1,604 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.controls +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Item wrapper + property var network: null + property bool isClosing: false + + readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" + + Connections { + target: root.wrapper + function onCurrentNameChanged() { + if (root.wrapper.currentName === "wirelesspassword") { + // Update network when popout becomes active + Qt.callLater(() => { + // Try to get network from parent Content's networkPopout + const content = root.parent?.parent?.parent; + if (content) { + const networkPopout = content.children.find(c => c.name === "network"); + if (networkPopout && networkPopout.item) { + root.network = networkPopout.item.passwordNetwork; + } + } + // Force focus to password container when popout becomes active + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + }); + } + } + } + + Timer { + id: focusTimer + interval: 150 + onTriggered: { + root.forceActiveFocus(); + passwordContainer.forceActiveFocus(); + } + } + + spacing: Appearance.spacing.normal + + implicitWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + + visible: shouldBeVisible || isClosing + enabled: shouldBeVisible && !isClosing + focus: enabled + + Component.onCompleted: { + if (shouldBeVisible) { + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + } + } + + onShouldBeVisibleChanged: { + if (shouldBeVisible) { + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + } + } + + Keys.onEscapePressed: closeDialog() + + StyledRect { + Layout.fillWidth: true + Layout.preferredWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surface + opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 + scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + ParallelAnimation { + running: root.isClosing + onFinished: { + if (root.isClosing) { + root.isClosing = false; + } + } + + Anim { + target: parent + property: "opacity" + to: 0 + } + Anim { + target: parent + property: "scale" + to: 0.7 + } + } + + Keys.onEscapePressed: root.closeDialog() + + ColumnLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "lock" + font.pointSize: Appearance.font.size.extraLarge * 2 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enter password") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + id: networkNameText + Layout.alignment: Qt.AlignHCenter + text: { + if (root.network) { + const ssid = root.network.ssid; + if (ssid && ssid.length > 0) { + return qsTr("Network: %1").arg(ssid); + } + } + return qsTr("Network: Unknown"); + } + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + Timer { + interval: 50 + running: root.shouldBeVisible && (!root.network || !root.network.ssid) + repeat: true + property int attempts: 0 + onTriggered: { + attempts++; + // Keep trying to get network from Network component + const content = root.parent?.parent?.parent; + if (content) { + const networkPopout = content.children.find(c => c.name === "network"); + if (networkPopout && networkPopout.item && networkPopout.item.passwordNetwork) { + root.network = networkPopout.item.passwordNetwork; + } + } + // Stop if we got it or after 20 attempts (1 second) + if ((root.network && root.network.ssid) || attempts >= 20) { + stop(); + attempts = 0; + } + } + onRunningChanged: { + if (!running) { + attempts = 0; + } + } + } + + StyledText { + id: statusText + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.spacing.small + visible: connectButton.connecting || connectButton.hasError + text: { + if (connectButton.hasError) { + return qsTr("Connection failed. Please check your password and try again."); + } + if (connectButton.connecting) { + return qsTr("Connecting..."); + } + return ""; + } + color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + font.weight: 400 + wrapMode: Text.WordWrap + Layout.maximumWidth: parent.width - Appearance.padding.large * 2 + } + + FocusScope { + id: passwordContainer + objectName: "passwordContainer" + Layout.topMargin: Appearance.spacing.large + Layout.fillWidth: true + implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + + focus: true + activeFocusOnTab: true + + property string passwordBuffer: "" + + Keys.onPressed: event => { + // Ensure we have focus when receiving keyboard input + if (!activeFocus) { + forceActiveFocus(); + } + + // Clear error when user starts typing + if (connectButton.hasError && event.text && event.text.length > 0) { + connectButton.hasError = false; + } + + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { + if (connectButton.enabled) { + connectButton.clicked(); + } + event.accepted = true; + } else if (event.key === Qt.Key_Backspace) { + if (event.modifiers & Qt.ControlModifier) { + passwordBuffer = ""; + } else { + passwordBuffer = passwordBuffer.slice(0, -1); + } + event.accepted = true; + } else if (event.text && event.text.length > 0) { + passwordBuffer += event.text; + event.accepted = true; + } + } + + Connections { + target: root + function onShouldBeVisibleChanged(): void { + if (root.shouldBeVisible) { + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); + passwordContainer.passwordBuffer = ""; + connectButton.hasError = false; + } + } + } + + Timer { + id: passwordFocusTimer + interval: 50 + onTriggered: { + passwordContainer.forceActiveFocus(); + } + } + + Component.onCompleted: { + if (root.shouldBeVisible) { + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); + } + } + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.normal + color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer + border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) + border.color: { + if (connectButton.hasError) { + return Colours.palette.m3error; + } + if (passwordContainer.activeFocus) { + return Colours.palette.m3primary; + } + return root.shouldBeVisible ? Colours.palette.m3outline : "transparent"; + } + + Behavior on border.color { + CAnim {} + } + + Behavior on border.width { + CAnim {} + } + + Behavior on color { + CAnim {} + } + } + + StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor + radius: Appearance.rounding.normal + + function onClicked(): void { + passwordContainer.forceActiveFocus(); + } + } + + StyledText { + id: placeholder + + anchors.centerIn: parent + text: qsTr("Password") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + font.family: Appearance.font.family.mono + opacity: passwordContainer.passwordBuffer ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + + ListView { + id: charList + + readonly property int fullWidth: count * (implicitHeight + spacing) - spacing + + anchors.centerIn: parent + implicitWidth: fullWidth + implicitHeight: Appearance.font.size.normal + + orientation: Qt.Horizontal + spacing: Appearance.spacing.small / 2 + interactive: false + + model: ScriptModel { + values: passwordContainer.passwordBuffer.split("") + } + + delegate: StyledRect { + id: ch + + implicitWidth: implicitHeight + implicitHeight: charList.implicitHeight + + color: Colours.palette.m3onSurface + radius: Appearance.rounding.small / 2 + + opacity: 0 + scale: 0 + Component.onCompleted: { + opacity = 1; + scale = 1; + } + ListView.onRemove: removeAnim.start() + + SequentialAnimation { + id: removeAnim + + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: true + } + ParallelAnimation { + Anim { + target: ch + property: "opacity" + to: 0 + } + Anim { + target: ch + property: "scale" + to: 0.5 + } + } + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: false + } + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } + + Behavior on implicitWidth { + Anim {} + } + } + } + + RowLayout { + Layout.topMargin: Appearance.spacing.normal + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + TextButton { + id: cancelButton + + Layout.fillWidth: true + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + inactiveColour: Colours.palette.m3secondaryContainer + inactiveOnColour: Colours.palette.m3onSecondaryContainer + text: qsTr("Cancel") + + onClicked: root.closeDialog() + } + + TextButton { + id: connectButton + + property bool connecting: false + property bool hasError: false + + Layout.fillWidth: true + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + inactiveColour: Colours.palette.m3primary + inactiveOnColour: Colours.palette.m3onPrimary + text: qsTr("Connect") + enabled: passwordContainer.passwordBuffer.length > 0 && !connecting + + onClicked: { + if (!root.network || connecting) { + return; + } + + const password = passwordContainer.passwordBuffer; + if (!password || password.length === 0) { + return; + } + + // Clear any previous error + hasError = false; + + // Set connecting state + connecting = true; + enabled = false; + text = qsTr("Connecting..."); + + // Connect to network + Nmcli.connectToNetwork(root.network.ssid, password, root.network.bssid || "", result => { + if (result && result.success) + // Connection successful, monitor will handle the rest + {} else if (result && result.needsPassword) { + // Shouldn't happen since we provided password + connectionMonitor.stop(); + connecting = false; + hasError = true; + enabled = true; + text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } else { + // Connection failed immediately - show error + connectionMonitor.stop(); + connecting = false; + hasError = true; + enabled = true; + text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + }); + + // Start monitoring connection + connectionMonitor.start(); + } + } + } + } + } + + function checkConnectionStatus(): void { + if (!root.shouldBeVisible || !connectButton.connecting) { + return; + } + + // Check if we're connected to the target network (case-insensitive SSID comparison) + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + + if (isConnected) { + // Successfully connected - give it a moment for network list to update + // Use Timer for actual delay + connectionSuccessTimer.start(); + return; + } + + // Check for connection failures - if pending connection was cleared but we're not connected + if (Nmcli.pendingConnection === null && connectButton.connecting) { + // Wait a bit more before giving up (allow time for connection to establish) + if (connectionMonitor.repeatCount > 10) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + } + } + + Timer { + id: connectionMonitor + interval: 1000 + repeat: true + triggeredOnStart: false + property int repeatCount: 0 + + onTriggered: { + repeatCount++; + root.checkConnectionStatus(); + } + + onRunningChanged: { + if (!running) { + repeatCount = 0; + } + } + } + + Timer { + id: connectionSuccessTimer + interval: 500 + onTriggered: { + // Double-check connection is still active + if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { + const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + // Return to network popout on successful connection + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + closeDialog(); + } + } + } + } + + Connections { + target: Nmcli + function onActiveChanged() { + if (root.shouldBeVisible) { + root.checkConnectionStatus(); + } + } + function onConnectionFailed(ssid: string) { + if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + Nmcli.forgetNetwork(ssid); + } + } + } + + function closeDialog(): void { + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; + connectButton.connecting = false; + connectButton.hasError = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); + + // Return to network popout + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + } +} + diff --git a/modules/bar/popouts/WirelessPasswordPopout.qml b/modules/bar/popouts/WirelessPasswordPopout.qml deleted file mode 100644 index 59a15b9..0000000 --- a/modules/bar/popouts/WirelessPasswordPopout.qml +++ /dev/null @@ -1,603 +0,0 @@ -pragma ComponentBehavior: Bound - -import qs.components -import qs.components.controls -import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts - -ColumnLayout { - id: root - - required property Item wrapper - property var network: null - property bool isClosing: false - - readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" - - Connections { - target: root.wrapper - function onCurrentNameChanged() { - if (root.wrapper.currentName === "wirelesspassword") { - // Update network when popout becomes active - Qt.callLater(() => { - // Try to get network from parent Content's networkPopout - const content = root.parent?.parent?.parent; - if (content) { - const networkPopout = content.children.find(c => c.name === "network"); - if (networkPopout && networkPopout.item) { - root.network = networkPopout.item.passwordNetwork; - } - } - // Force focus to password container when popout becomes active - // Use Timer for actual delay to ensure dialog is fully rendered - focusTimer.start(); - }); - } - } - } - - Timer { - id: focusTimer - interval: 150 - onTriggered: { - root.forceActiveFocus(); - passwordContainer.forceActiveFocus(); - } - } - - spacing: Appearance.spacing.normal - - implicitWidth: 400 - implicitHeight: content.implicitHeight + Appearance.padding.large * 2 - - visible: shouldBeVisible || isClosing - enabled: shouldBeVisible && !isClosing - focus: enabled - - Component.onCompleted: { - if (shouldBeVisible) { - // Use Timer for actual delay to ensure dialog is fully rendered - focusTimer.start(); - } - } - - onShouldBeVisibleChanged: { - if (shouldBeVisible) { - // Use Timer for actual delay to ensure dialog is fully rendered - focusTimer.start(); - } - } - - Keys.onEscapePressed: closeDialog() - - StyledRect { - Layout.fillWidth: true - Layout.preferredWidth: 400 - implicitHeight: content.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surface - opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 - scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim {} - } - - ParallelAnimation { - running: root.isClosing - onFinished: { - if (root.isClosing) { - root.isClosing = false; - } - } - - Anim { - target: parent - property: "opacity" - to: 0 - } - Anim { - target: parent - property: "scale" - to: 0.7 - } - } - - Keys.onEscapePressed: root.closeDialog() - - ColumnLayout { - id: content - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - - spacing: Appearance.spacing.normal - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "lock" - font.pointSize: Appearance.font.size.extraLarge * 2 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Enter password") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - - StyledText { - id: networkNameText - Layout.alignment: Qt.AlignHCenter - text: { - if (root.network) { - const ssid = root.network.ssid; - if (ssid && ssid.length > 0) { - return qsTr("Network: %1").arg(ssid); - } - } - return qsTr("Network: Unknown"); - } - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } - - Timer { - interval: 50 - running: root.shouldBeVisible && (!root.network || !root.network.ssid) - repeat: true - property int attempts: 0 - onTriggered: { - attempts++; - // Keep trying to get network from Network component - const content = root.parent?.parent?.parent; - if (content) { - const networkPopout = content.children.find(c => c.name === "network"); - if (networkPopout && networkPopout.item && networkPopout.item.passwordNetwork) { - root.network = networkPopout.item.passwordNetwork; - } - } - // Stop if we got it or after 20 attempts (1 second) - if ((root.network && root.network.ssid) || attempts >= 20) { - stop(); - attempts = 0; - } - } - onRunningChanged: { - if (!running) { - attempts = 0; - } - } - } - - StyledText { - id: statusText - - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.spacing.small - visible: connectButton.connecting || connectButton.hasError - text: { - if (connectButton.hasError) { - return qsTr("Connection failed. Please check your password and try again."); - } - if (connectButton.connecting) { - return qsTr("Connecting..."); - } - return ""; - } - color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small - font.weight: 400 - wrapMode: Text.WordWrap - Layout.maximumWidth: parent.width - Appearance.padding.large * 2 - } - - FocusScope { - id: passwordContainer - objectName: "passwordContainer" - Layout.topMargin: Appearance.spacing.large - Layout.fillWidth: true - implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) - - focus: true - activeFocusOnTab: true - - property string passwordBuffer: "" - - Keys.onPressed: event => { - // Ensure we have focus when receiving keyboard input - if (!activeFocus) { - forceActiveFocus(); - } - - // Clear error when user starts typing - if (connectButton.hasError && event.text && event.text.length > 0) { - connectButton.hasError = false; - } - - if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { - if (connectButton.enabled) { - connectButton.clicked(); - } - event.accepted = true; - } else if (event.key === Qt.Key_Backspace) { - if (event.modifiers & Qt.ControlModifier) { - passwordBuffer = ""; - } else { - passwordBuffer = passwordBuffer.slice(0, -1); - } - event.accepted = true; - } else if (event.text && event.text.length > 0) { - passwordBuffer += event.text; - event.accepted = true; - } - } - - Connections { - target: root - function onShouldBeVisibleChanged(): void { - if (root.shouldBeVisible) { - // Use Timer for actual delay to ensure focus works correctly - passwordFocusTimer.start(); - passwordContainer.passwordBuffer = ""; - connectButton.hasError = false; - } - } - } - - Timer { - id: passwordFocusTimer - interval: 50 - onTriggered: { - passwordContainer.forceActiveFocus(); - } - } - - Component.onCompleted: { - if (root.shouldBeVisible) { - // Use Timer for actual delay to ensure focus works correctly - passwordFocusTimer.start(); - } - } - - StyledRect { - anchors.fill: parent - radius: Appearance.rounding.normal - color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer - border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) - border.color: { - if (connectButton.hasError) { - return Colours.palette.m3error; - } - if (passwordContainer.activeFocus) { - return Colours.palette.m3primary; - } - return root.shouldBeVisible ? Colours.palette.m3outline : "transparent"; - } - - Behavior on border.color { - CAnim {} - } - - Behavior on border.width { - CAnim {} - } - - Behavior on color { - CAnim {} - } - } - - StateLayer { - hoverEnabled: false - cursorShape: Qt.IBeamCursor - radius: Appearance.rounding.normal - - function onClicked(): void { - passwordContainer.forceActiveFocus(); - } - } - - StyledText { - id: placeholder - - anchors.centerIn: parent - text: qsTr("Password") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono - opacity: passwordContainer.passwordBuffer ? 0 : 1 - - Behavior on opacity { - Anim {} - } - } - - ListView { - id: charList - - readonly property int fullWidth: count * (implicitHeight + spacing) - spacing - - anchors.centerIn: parent - implicitWidth: fullWidth - implicitHeight: Appearance.font.size.normal - - orientation: Qt.Horizontal - spacing: Appearance.spacing.small / 2 - interactive: false - - model: ScriptModel { - values: passwordContainer.passwordBuffer.split("") - } - - delegate: StyledRect { - id: ch - - implicitWidth: implicitHeight - implicitHeight: charList.implicitHeight - - color: Colours.palette.m3onSurface - radius: Appearance.rounding.small / 2 - - opacity: 0 - scale: 0 - Component.onCompleted: { - opacity = 1; - scale = 1; - } - ListView.onRemove: removeAnim.start() - - SequentialAnimation { - id: removeAnim - - PropertyAction { - target: ch - property: "ListView.delayRemove" - value: true - } - ParallelAnimation { - Anim { - target: ch - property: "opacity" - to: 0 - } - Anim { - target: ch - property: "scale" - to: 0.5 - } - } - PropertyAction { - target: ch - property: "ListView.delayRemove" - value: false - } - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } - } - - Behavior on implicitWidth { - Anim {} - } - } - } - - RowLayout { - Layout.topMargin: Appearance.spacing.normal - Layout.fillWidth: true - spacing: Appearance.spacing.normal - - TextButton { - id: cancelButton - - Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 - inactiveColour: Colours.palette.m3secondaryContainer - inactiveOnColour: Colours.palette.m3onSecondaryContainer - text: qsTr("Cancel") - - onClicked: root.closeDialog() - } - - TextButton { - id: connectButton - - property bool connecting: false - property bool hasError: false - - Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 - inactiveColour: Colours.palette.m3primary - inactiveOnColour: Colours.palette.m3onPrimary - text: qsTr("Connect") - enabled: passwordContainer.passwordBuffer.length > 0 && !connecting - - onClicked: { - if (!root.network || connecting) { - return; - } - - const password = passwordContainer.passwordBuffer; - if (!password || password.length === 0) { - return; - } - - // Clear any previous error - hasError = false; - - // Set connecting state - connecting = true; - enabled = false; - text = qsTr("Connecting..."); - - // Connect to network - Nmcli.connectToNetwork(root.network.ssid, password, root.network.bssid || "", result => { - if (result && result.success) - // Connection successful, monitor will handle the rest - {} else if (result && result.needsPassword) { - // Shouldn't happen since we provided password - connectionMonitor.stop(); - connecting = false; - hasError = true; - enabled = true; - text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } else { - // Connection failed immediately - show error - connectionMonitor.stop(); - connecting = false; - hasError = true; - enabled = true; - text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } - }); - - // Start monitoring connection - connectionMonitor.start(); - } - } - } - } - } - - function checkConnectionStatus(): void { - if (!root.shouldBeVisible || !connectButton.connecting) { - return; - } - - // Check if we're connected to the target network (case-insensitive SSID comparison) - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - - if (isConnected) { - // Successfully connected - give it a moment for network list to update - // Use Timer for actual delay - connectionSuccessTimer.start(); - return; - } - - // Check for connection failures - if pending connection was cleared but we're not connected - if (Nmcli.pendingConnection === null && connectButton.connecting) { - // Wait a bit more before giving up (allow time for connection to establish) - if (connectionMonitor.repeatCount > 10) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } - } - } - - Timer { - id: connectionMonitor - interval: 1000 - repeat: true - triggeredOnStart: false - property int repeatCount: 0 - - onTriggered: { - repeatCount++; - root.checkConnectionStatus(); - } - - onRunningChanged: { - if (!running) { - repeatCount = 0; - } - } - } - - Timer { - id: connectionSuccessTimer - interval: 500 - onTriggered: { - // Double-check connection is still active - if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { - const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - if (stillConnected) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - // Return to network popout on successful connection - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; - } - closeDialog(); - } - } - } - } - - Connections { - target: Nmcli - function onActiveChanged() { - if (root.shouldBeVisible) { - root.checkConnectionStatus(); - } - } - function onConnectionFailed(ssid: string) { - if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - Nmcli.forgetNetwork(ssid); - } - } - } - - function closeDialog(): void { - if (isClosing) { - return; - } - - isClosing = true; - passwordContainer.passwordBuffer = ""; - connectButton.connecting = false; - connectButton.hasError = false; - connectButton.text = qsTr("Connect"); - connectionMonitor.stop(); - - // Return to network popout - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; - } - } -} -- cgit v1.2.3-freya