diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 21d5389..3ba335d 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -41,7 +41,9 @@ NLoader { // Also handle visibility changes from external sources onVisibleChanged: { - if (!visible && wifiMenuRect.opacityValue > 0) { + if (visible && Settings.data.network.wifiEnabled) { + network.refreshNetworks() + } else if (wifiMenuRect.opacityValue > 0) { // Start hide animation wifiMenuRect.scaleValue = 0.8 wifiMenuRect.opacityValue = 0.0 @@ -65,6 +67,22 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + Network { + id: network + } + + // Timer to refresh networks when WiFi is enabled while menu is open + Timer { + id: wifiEnableRefreshTimer + interval: 3000 // Wait 3 seconds for WiFi to be fully ready + repeat: false + onTriggered: { + if (Settings.data.network.wifiEnabled && wifiPanel.visible) { + network.refreshNetworks() + } + } + } + Rectangle { id: wifiMenuRect color: Colors.mSurface @@ -135,14 +153,19 @@ NLoader { value: Settings.data.network.wifiEnabled onToggled: function (value) { Settings.data.network.wifiEnabled = value - // TBC: This should be done in a service - Quickshell.execDetached(["nmcli", "radio", "wifi", Settings.data.network.wifiEnabled ? "on" : "off"]) + network.setWifiEnabled(value) + + // If enabling WiFi while menu is open, refresh after a delay + if (value) { + wifiEnableRefreshTimer.start() + } } } NIconButton { icon: "refresh" sizeMultiplier: 0.8 + enabled: Settings.data.network.wifiEnabled && !network.isLoading onClicked: { network.refreshNetworks() } @@ -159,226 +182,282 @@ NLoader { NDivider {} - ListView { - id: networkList + Item { Layout.fillWidth: true Layout.fillHeight: true - model: Object.values(network.networks) - spacing: Style.marginMedium * scaling - clip: true - delegate: Item { - width: parent.width - height: modelData.ssid === passwordPromptSsid - && showPasswordPrompt ? 108 * scaling : Style.baseWidgetSize * 1.5 * scaling + // Loading indicator + ColumnLayout { + anchors.centerIn: parent + visible: Settings.data.network.wifiEnabled && network.isLoading + spacing: Style.marginMedium * scaling - ColumnLayout { - anchors.fill: parent - spacing: 0 + NBusyIndicator { + running: network.isLoading + color: Colors.mPrimary + size: Style.baseWidgetSize * scaling + Layout.alignment: Qt.AlignHCenter + } - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling - radius: Style.radiusMedium * scaling - color: modelData.connected ? Colors.mPrimary : (networkMouseArea.containsMouse ? Colors.mTertiary : "transparent") + NText { + text: "Scanning for networks..." + font.pointSize: Style.fontSizeNormal * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - spacing: Style.marginSmall * scaling + // WiFi disabled message + ColumnLayout { + anchors.centerIn: parent + visible: !Settings.data.network.wifiEnabled + spacing: Style.marginMedium * scaling - NText { - text: network.signalIcon(modelData.signal) - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXL * scaling - color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) - } + NText { + text: "wifi_off" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXXL * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } - ColumnLayout { - Layout.fillWidth: true - spacing: Style.marginTiny * scaling + NText { + text: "WiFi is disabled" + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Enable WiFi to see available networks" + font.pointSize: Style.fontSizeNormal * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + + // Network list + ListView { + id: networkList + anchors.fill: parent + visible: Settings.data.network.wifiEnabled && !network.isLoading + model: Object.values(network.networks) + spacing: Style.marginMedium * scaling + clip: true + + delegate: Item { + width: parent.width + height: modelData.ssid === passwordPromptSsid + && showPasswordPrompt ? 108 * scaling : Style.baseWidgetSize * 1.5 * scaling + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling + radius: Style.radiusMedium * scaling + color: modelData.connected ? Colors.mPrimary : (networkMouseArea.containsMouse ? Colors.mTertiary : "transparent") + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling - // SSID NText { - text: modelData.ssid || "Unknown Network" - font.pointSize: Style.fontSizeNormal * scaling - elide: Text.ElideRight - Layout.fillWidth: true + text: network.signalIcon(modelData.signal) + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) } - // Security Protocol - NText { - text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" - font.pointSize: Style.fontSizeTiny * scaling - elide: Text.ElideRight + ColumnLayout { Layout.fillWidth: true + spacing: Style.marginTiny * scaling + + // SSID + NText { + text: modelData.ssid || "Unknown Network" + font.pointSize: Style.fontSizeNormal * scaling + elide: Text.ElideRight + Layout.fillWidth: true + color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + } + + // Security Protocol + NText { + text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" + font.pointSize: Style.fontSizeTiny * scaling + elide: Text.ElideRight + Layout.fillWidth: true + color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + } + + NText { + visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" + && network.connectError.length > 0 + text: network.connectError + color: Colors.mError + font.pointSize: Style.fontSizeSmall * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + Item { + Layout.preferredWidth: 22 + Layout.preferredHeight: 22 + visible: network.connectStatusSsid === modelData.ssid + && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) + + NBusyIndicator { + visible: network.connectingSsid === modelData.ssid + running: network.connectingSsid === modelData.ssid + color: Colors.mPrimary + anchors.centerIn: parent + size: Style.baseWidgetSize * 0.7 * scaling + } + + // TBC: Does nothing on my setup + NText { + visible: network.connectStatus === "success" && !network.connectingSsid + text: "check_circle" + font.family: "Material Symbols Outlined" + font.pointSize: 18 * scaling + color: "#43a047" // TBC: No! + anchors.centerIn: parent + } + + // TBC: Does nothing on my setup + NText { + visible: network.connectStatus === "error" && !network.connectingSsid + text: "error" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mError + anchors.centerIn: parent + } + } + + NText { + visible: modelData.connected + text: "connected" + font.pointSize: Style.fontSizeSmall * scaling color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) } + } - NText { - visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" - && network.connectError.length > 0 - text: network.connectError - color: Colors.mError - font.pointSize: Style.fontSizeSmall * scaling - elide: Text.ElideRight + MouseArea { + id: networkMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + if (modelData.connected) { + network.disconnectNetwork(modelData.ssid) + } else if (network.isSecured(modelData.security) && !modelData.existing) { + passwordPromptSsid = modelData.ssid + showPasswordPrompt = true + passwordInput = "" // Clear previous input + Qt.callLater(function () { + passwordInputField.forceActiveFocus() + }) + } else { + network.connectNetwork(modelData.ssid, modelData.security) + } + } + } + } + + // Password prompt section + Rectangle { + id: passwordPromptSection + Layout.fillWidth: true + Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 + Layout.margins: 8 + visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt + color: Colors.mSurfaceVariant + radius: Style.radiusSmall * scaling + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling + + Item { Layout.fillWidth: true + Layout.preferredHeight: 36 + + Rectangle { + anchors.fill: parent + radius: 8 + color: "transparent" + border.color: passwordInputField.activeFocus ? Colors.mPrimary : Colors.mOutline + border.width: 1 + + TextInput { + id: passwordInputField + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + text: passwordInput + font.pointSize: Style.fontSizeMedium * scaling + color: Colors.mOnSurface + verticalAlignment: TextInput.AlignVCenter + clip: true + focus: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhNone + echoMode: TextInput.Password + onTextChanged: passwordInput = text + onAccepted: { + network.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + } + + MouseArea { + id: passwordInputMouseArea + anchors.fill: parent + onClicked: passwordInputField.forceActiveFocus() + } + } + } } - } - - Item { - Layout.preferredWidth: 22 - Layout.preferredHeight: 22 - visible: network.connectStatusSsid === modelData.ssid - && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) - - NBusyIndicator { - visible: network.connectingSsid === modelData.ssid - running: network.connectingSsid === modelData.ssid - color: Colors.mPrimary - anchors.centerIn: parent - size: Style.baseWidgetSize * 0.7 * scaling - } - - // TBC: Does nothing on my setup - NText { - visible: network.connectStatus === "success" && !network.connectingSsid - text: "check_circle" - font.family: "Material Symbols Outlined" - font.pointSize: 18 * scaling - color: "#43a047" // TBC: No! - anchors.centerIn: parent - } - - // TBC: Does nothing on my setup - NText { - visible: network.connectStatus === "error" && !network.connectingSsid - text: "error" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mError - anchors.centerIn: parent - } - } - - NText { - visible: modelData.connected - text: "connected" - font.pointSize: Style.fontSizeSmall * scaling - color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) - } - } - - MouseArea { - id: networkMouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - if (modelData.connected) { - network.disconnectNetwork(modelData.ssid) - } else if (network.isSecured(modelData.security) && !modelData.existing) { - passwordPromptSsid = modelData.ssid - showPasswordPrompt = true - passwordInput = "" // Clear previous input - Qt.callLater(function () { - passwordInputField.forceActiveFocus() - }) - } else { - network.connectNetwork(modelData.ssid, modelData.security) - } - } - } - } - - // Password prompt section - Rectangle { - id: passwordPromptSection - Layout.fillWidth: true - Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 - Layout.margins: 8 - visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt - color: Colors.mSurfaceVariant - radius: Style.radiusSmall * scaling - - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - spacing: Style.marginSmall * scaling - - Item { - Layout.fillWidth: true - Layout.preferredHeight: 36 Rectangle { - anchors.fill: parent - radius: 8 - color: "transparent" - border.color: passwordInputField.activeFocus ? Colors.mPrimary : Colors.mOutline - border.width: 1 + Layout.preferredWidth: 80 + Layout.preferredHeight: 36 + radius: Style.radiusMedium * scaling + color: Colors.mPrimary + border.color: Colors.mPrimary + border.width: 0 - TextInput { - id: passwordInputField + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: "Connect" + color: Colors.mSurface + font.pointSize: Style.fontSizeSmall * scaling + } + + MouseArea { anchors.fill: parent - anchors.margins: Style.marginMedium * scaling - text: passwordInput - font.pointSize: Style.fontSizeMedium * scaling - color: Colors.mOnSurface - verticalAlignment: TextInput.AlignVCenter - clip: true - focus: true - selectByMouse: true - activeFocusOnTab: true - inputMethodHints: Qt.ImhNone - echoMode: TextInput.Password - onTextChanged: passwordInput = text - onAccepted: { + onClicked: { network.submitPassword(passwordPromptSsid, passwordInput) showPasswordPrompt = false } - - MouseArea { - id: passwordInputMouseArea - anchors.fill: parent - onClicked: passwordInputField.forceActiveFocus() - } + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = Qt.darker(Colors.mPrimary, 1.1) + onExited: parent.color = Colors.mPrimary } } } - - Rectangle { - Layout.preferredWidth: 80 - Layout.preferredHeight: 36 - radius: Style.radiusMedium * scaling - color: Colors.mPrimary - border.color: Colors.mPrimary - border.width: 0 - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - - NText { - anchors.centerIn: parent - text: "Connect" - color: Colors.mSurface - font.pointSize: Style.fontSizeSmall * scaling - } - - MouseArea { - anchors.fill: parent - onClicked: { - network.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - } - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: parent.color = Qt.darker(Colors.mPrimary, 1.1) - onExited: parent.color = Colors.mPrimary - } - } } } } @@ -388,4 +467,4 @@ NLoader { } } } -} +} \ No newline at end of file diff --git a/Services/Network.qml b/Services/Network.qml index 92527b8..bd8c70f 100644 --- a/Services/Network.qml +++ b/Services/Network.qml @@ -1,4 +1,5 @@ import QtQuick +import Quickshell import Quickshell.Io QtObject { @@ -10,6 +11,8 @@ QtObject { property string connectStatusSsid: "" property string connectError: "" property string detectedInterface: "" + property string lastConnectedNetwork: "" + property bool isLoading: false function signalIcon(signal) { if (signal >= 80) @@ -28,9 +31,29 @@ QtObject { } function refreshNetworks() { + isLoading = true existingNetwork.running = true } + function setWifiEnabled(enabled) { + if (enabled) { + // Enable WiFi radio + isLoading = true + enableWifiProcess.running = true + } else { + // Store the currently connected network before disabling + for (const ssid in networks) { + if (networks[ssid].connected) { + lastConnectedNetwork = ssid + break + } + } + + // Disable WiFi radio + disableWifiProcess.running = true + } + } + function connectNetwork(ssid, security) { pendingConnect = { "ssid": ssid, @@ -87,7 +110,7 @@ QtObject { property int refreshInterval: 25000 - // Only refresh when we have an active connection + // Only refresh when we have an active connection and WiFi is enabled property bool hasActiveConnection: { for (const net in networks) { if (networks[net].connected) { @@ -99,18 +122,111 @@ QtObject { property Timer refreshTimer: Timer { interval: root.refreshInterval - // Only run timer when we're connected to a network - running: root.hasActiveConnection + // Only run timer when we're connected to a network and WiFi is enabled + running: root.hasActiveConnection && Settings.data.network.wifiEnabled repeat: true onTriggered: root.refreshNetworks() } // Force a refresh when menu is opened function onMenuOpened() { - refreshNetworks() + if (Settings.data.network.wifiEnabled) { + refreshNetworks() + } } - function onMenuClosed() {// No need to do anything special on close + function onMenuClosed() { + // No need to do anything special on close + } + + // Process to enable WiFi radio + property Process enableWifiProcess: Process { + id: enableWifiProcess + running: false + command: ["nmcli", "radio", "wifi", "on"] + onRunningChanged: { + if (!running) { + // Wait a moment for the radio to be enabled, then refresh networks + enableWifiDelayTimer.start() + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim() !== "") { + console.warn("Error enabling WiFi:", text) + } + } + } + } + + // Timer to delay network refresh after enabling WiFi + property Timer enableWifiDelayTimer: Timer { + id: enableWifiDelayTimer + interval: 2000 // Wait 2 seconds for radio to be ready + repeat: false + onTriggered: { + // Force refresh networks multiple times to ensure UI updates + root.refreshNetworks() + + // Try to auto-reconnect to the last connected network if it exists + if (lastConnectedNetwork) { + autoReconnectTimer.start() + } + + // Set up additional refresh to ensure UI is populated + postEnableRefreshTimer.start() + } + } + + // Additional timer to ensure networks are populated after enabling + property Timer postEnableRefreshTimer: Timer { + id: postEnableRefreshTimer + interval: 1000 + repeat: false + onTriggered: { + root.refreshNetworks() + } + } + + // Timer to attempt auto-reconnection to the last connected network + property Timer autoReconnectTimer: Timer { + id: autoReconnectTimer + interval: 3000 // Wait 3 seconds after scan for networks to be available + repeat: false + onTriggered: { + if (lastConnectedNetwork && networks[lastConnectedNetwork]) { + const network = networks[lastConnectedNetwork] + if (network.existing && !network.connected) { + upConnectionProcess.profileName = lastConnectedNetwork + upConnectionProcess.running = true + } + } + } + } + + // Process to disable WiFi radio + property Process disableWifiProcess: Process { + id: disableWifiProcess + running: false + command: ["nmcli", "radio", "wifi", "off"] + onRunningChanged: { + if (!running) { + // Clear networks when WiFi is disabled + root.networks = ({}) + root.connectingSsid = "" + root.connectStatus = "" + root.connectStatusSsid = "" + root.connectError = "" + root.isLoading = false + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim() !== "") { + console.warn("Error disabling WiFi:", text) + } + } + } } property Process disconnectProfileProcess: Process { @@ -211,6 +327,7 @@ QtObject { } root.networks = networksMap + root.isLoading = false scanProcess.existingNetwork = {} } } @@ -235,6 +352,7 @@ QtObject { root.connectStatus = "success" root.connectStatusSsid = connectProcess.ssid root.connectError = "" + root.lastConnectedNetwork = connectProcess.ssid root.refreshNetworks() } } @@ -322,8 +440,9 @@ QtObject { onStreamFinished: { root.connectingSsid = "" root.connectStatus = "success" - root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : "" + root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : profileName root.connectError = "" + root.lastConnectedNetwork = profileName root.pendingConnect = null root.refreshNetworks() } @@ -332,7 +451,7 @@ QtObject { onStreamFinished: { root.connectingSsid = "" root.connectStatus = "error" - root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : "" + root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : profileName root.connectError = text root.pendingConnect = null } @@ -340,6 +459,9 @@ QtObject { } Component.onCompleted: { - refreshNetworks() + // Only refresh networks if WiFi is enabled + if (Settings.data.network.wifiEnabled) { + refreshNetworks() + } } -} +} \ No newline at end of file