From 4c9a51bf6a9720c3f0effd7d286119e628a67090 Mon Sep 17 00:00:00 2001 From: JPratama7 Date: Sun, 3 Aug 2025 06:29:57 +0700 Subject: [PATCH 01/11] chore: add Program --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fed6799..b8d3939 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .qmlls.ini -notification_history.json \ No newline at end of file +notification_history.json +Programs \ No newline at end of file From d2ea87b46b2bf5f0ed132a9d07bd4cf9fd8c5ae8 Mon Sep 17 00:00:00 2001 From: JPratama7 Date: Sun, 3 Aug 2025 06:33:44 +0700 Subject: [PATCH 02/11] feat: add refresh button for wifi --- Widgets/Sidebar/Panel/WifiPanel.qml | 77 +++++++++++++++-------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/Widgets/Sidebar/Panel/WifiPanel.qml b/Widgets/Sidebar/Panel/WifiPanel.qml index 008a3c1..3c115ef 100644 --- a/Widgets/Sidebar/Panel/WifiPanel.qml +++ b/Widgets/Sidebar/Panel/WifiPanel.qml @@ -34,42 +34,40 @@ Item { } stdout: StdioCollector { onStreamFinished: { - var lines = text.split("\n"); - var nets = []; - var seen = {}; - for (var i = 0; i < lines.length; ++i) { - var line = lines[i].trim(); + const lines = text.split("\n"); + const networksMap = {}; + + for (let i = 0; i < lines.length; ++i) { + const line = lines[i].trim(); if (!line) continue; - var parts = line.split(":"); - var ssid = parts[0]; - var security = parts[1]; - var signal = parseInt(parts[2]); - var inUse = parts[3] === "*"; + + const parts = line.split(":"); + if (parts.length < 4) { + console.warn("Malformed nmcli output line:", line); + continue; + } + + const ssid = parts[0]; + const security = parts[1]; + const signal = parseInt(parts[2]); + const inUse = parts[3] === "*"; + if (ssid) { - if (!seen[ssid]) { - // First time seeing this SSID - nets.push({ ssid: ssid, security: security, signal: signal, connected: inUse }); - seen[ssid] = true; + if (!networksMap[ssid]) { + networksMap[ssid] = { ssid: ssid, security: security, signal: signal, connected: inUse }; } else { - // SSID already exists, update if this entry has better signal or is connected - for (var j = 0; j < nets.length; ++j) { - if (nets[j].ssid === ssid) { - // Update connection status if this entry is connected - if (inUse) { - nets[j].connected = true; - } - // Update signal if this entry has better signal - if (signal > nets[j].signal) { - nets[j].signal = signal; - nets[j].security = security; - } - break; - } + const existingNet = networksMap[ssid]; + if (inUse) { + existingNet.connected = true; + } + if (signal > existingNet.signal) { + existingNet.signal = signal; + existingNet.security = security; } } } } - wifiLogic.networks = nets; + wifiLogic.networks = Object.values(networksMap); } } } @@ -115,17 +113,17 @@ Item { listConnectionsProcess.running = true; } function doConnect() { - var params = wifiLogic.pendingConnect; + const params = wifiLogic.pendingConnect; wifiLogic.connectingSsid = params.ssid; if (params.security && params.security !== "--") { getInterfaceProcess.running = true; - } else { - connectProcess.security = params.security; - connectProcess.ssid = params.ssid; - connectProcess.password = params.password; - connectProcess.running = true; - wifiLogic.pendingConnect = null; + return; } + connectProcess.security = params.security; + connectProcess.ssid = params.ssid; + connectProcess.password = params.password; + connectProcess.running = true; + wifiLogic.pendingConnect = null; } function isSecured(security) { return security && security.trim() !== "" && security.trim() !== "--"; @@ -400,8 +398,13 @@ Item { color: Theme.textPrimary Layout.fillWidth: true } + Item { Layout.fillWidth: true } + IconButton { + icon: "refresh" + onClicked: wifiLogic.refreshNetworks() + } Rectangle { - width: 36; height: 36; radius: 18 + implicitWidth: 36; implicitHeight: 36; radius: 18 color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent" border.color: Theme.accentPrimary border.width: 1 From a419e56690aaee87dfd95e0ce75039dc51b404b8 Mon Sep 17 00:00:00 2001 From: JPratama7 Date: Sun, 3 Aug 2025 07:01:31 +0700 Subject: [PATCH 03/11] feat: add loading when scanning wifi --- Widgets/Sidebar/Panel/WifiPanel.qml | 41 ++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/Widgets/Sidebar/Panel/WifiPanel.qml b/Widgets/Sidebar/Panel/WifiPanel.qml index 3c115ef..127ad4f 100644 --- a/Widgets/Sidebar/Panel/WifiPanel.qml +++ b/Widgets/Sidebar/Panel/WifiPanel.qml @@ -30,7 +30,10 @@ Item { running: false command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] onRunningChanged: { - // Removed debug log + console.log("scanProcess.running changed: " + running); + // if (!running) { + // console.log("scanProcess finished."); + // } } stdout: StdioCollector { onStreamFinished: { @@ -399,10 +402,22 @@ Item { Layout.fillWidth: true } Item { Layout.fillWidth: true } + Spinner { + id: refreshIndicator + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + Layout.alignment: Qt.AlignVCenter + visible: scanProcess.running + running: scanProcess.running + color: Theme.accentPrimary // Assuming Spinner supports color property + size: 22 // Based on the existing Spinner usage + } IconButton { + id: refreshButton icon: "refresh" onClicked: wifiLogic.refreshNetworks() } + Rectangle { implicitWidth: 36; implicitHeight: 36; radius: 18 color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent" @@ -557,18 +572,18 @@ Item { verticalAlignment: Text.AlignVCenter Layout.alignment: Qt.AlignVCenter } - Item { - Layout.alignment: Qt.AlignVCenter - Layout.preferredHeight: 22 - Layout.preferredWidth: 22 - Spinner { - visible: wifiLogic.connectingSsid === modelData.ssid - running: wifiLogic.connectingSsid === modelData.ssid - color: Theme.accentPrimary - anchors.centerIn: parent - size: 22 - } - } + Item { + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: 22 + Layout.preferredWidth: 22 + Spinner { + visible: wifiLogic.connectingSsid === modelData.ssid + running: wifiLogic.connectingSsid === modelData.ssid + color: Theme.accentPrimary + anchors.centerIn: parent + size: 22 + } + } } MouseArea { id: networkMouseArea From fbea11ee9c10d7c4d698a0c73ac0d5c579c9fc07 Mon Sep 17 00:00:00 2001 From: JPratama7 Date: Sun, 3 Aug 2025 08:35:53 +0700 Subject: [PATCH 04/11] feat: add on refresh while still connecting --- Widgets/Sidebar/Panel/WifiPanel.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Widgets/Sidebar/Panel/WifiPanel.qml b/Widgets/Sidebar/Panel/WifiPanel.qml index 127ad4f..dc696c1 100644 --- a/Widgets/Sidebar/Panel/WifiPanel.qml +++ b/Widgets/Sidebar/Panel/WifiPanel.qml @@ -198,6 +198,12 @@ Item { property string password: "" property string security: "" running: false + onStarted: { + refreshIndicator.running = true; + } + onExited: (exitCode, exitStatus) => { + refreshIndicator.running = false; + } command: { if (password) { return ["nmcli", "device", "wifi", "connect", ssid, "password", password] From 5ad7d731c0a53e8e7634094acf4194dc5cf592f0 Mon Sep 17 00:00:00 2001 From: JPratama7 Date: Sun, 3 Aug 2025 19:39:55 +0700 Subject: [PATCH 05/11] feat: improve wifi connection handling with existing network detection and action panel --- Widgets/Sidebar/Panel/WifiPanel.qml | 258 ++++++++++++++++++++-------- 1 file changed, 188 insertions(+), 70 deletions(-) diff --git a/Widgets/Sidebar/Panel/WifiPanel.qml b/Widgets/Sidebar/Panel/WifiPanel.qml index dc696c1..6a2083d 100644 --- a/Widgets/Sidebar/Panel/WifiPanel.qml +++ b/Widgets/Sidebar/Panel/WifiPanel.qml @@ -17,6 +17,10 @@ Item { wifiLogic.refreshNetworks(); } + Component.onCompleted: { + existingNetwork.running = true; + } + function signalIcon(signal) { if (signal >= 80) return "network_wifi"; if (signal >= 60) return "network_wifi_3_bar"; @@ -25,16 +29,48 @@ Item { return "wifi_0_bar"; } + Process { + id: existingNetwork + running: false + command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] + stdout: StdioCollector { + onStreamFinished: { + const lines = text.split("\n"); + const networksMap = {}; + + refreshIndicator.running = true; + refreshIndicator.visible = true; + + for (let i = 0; i < lines.length; ++i) { + const line = lines[i].trim(); + if (!line) continue; + + const parts = line.split(":"); + if (parts.length < 2) { + console.warn("Malformed nmcli output line:", line); + continue; + } + + const ssid = parts[0]; + const type = parts[1]; + + if (ssid) { + networksMap[ssid] = { ssid: ssid, type: type }; + } + } + scanProcess.existingNetwork = networksMap; + scanProcess.running = true; + } + } + } + Process { id: scanProcess running: false command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] - onRunningChanged: { - console.log("scanProcess.running changed: " + running); - // if (!running) { - // console.log("scanProcess finished."); - // } - } + + property var existingNetwork + stdout: StdioCollector { onStreamFinished: { const lines = text.split("\n"); @@ -49,7 +85,6 @@ Item { console.warn("Malformed nmcli output line:", line); continue; } - const ssid = parts[0]; const security = parts[1]; const signal = parseInt(parts[2]); @@ -57,7 +92,7 @@ Item { if (ssid) { if (!networksMap[ssid]) { - networksMap[ssid] = { ssid: ssid, security: security, signal: signal, connected: inUse }; + networksMap[ssid] = { ssid: ssid, security: security, signal: signal, connected: inUse, existing: ssid in scanProcess.existingNetwork }; } else { const existingNet = networksMap[ssid]; if (inUse) { @@ -71,6 +106,9 @@ Item { } } wifiLogic.networks = Object.values(networksMap); + console.log(JSON.stringify(wifiLogic.networks)); + refreshIndicator.running = false; + refreshIndicator.visible = false; } } } @@ -91,17 +129,15 @@ Item { property string connectSecurity: "" property var pendingConnect: null property string detectedInterface: "" + property string actionPanelSsid: "" - function profileNameForSsid(ssid) { - return "quickshell-" + ssid.replace(/[^a-zA-Z0-9]/g, "_"); - } function disconnectNetwork(ssid) { - var profileName = wifiLogic.profileNameForSsid(ssid); + const profileName = ssid; disconnectProfileProcess.connectionName = profileName; disconnectProfileProcess.running = true; } function refreshNetworks() { - scanProcess.running = true; + existingNetwork.running = true; } function showAt() { wifiPanelModal.visible = true; @@ -109,15 +145,37 @@ Item { } function connectNetwork(ssid, security) { wifiLogic.pendingConnect = {ssid: ssid, security: security, password: ""}; - listConnectionsProcess.running = true; + wifiLogic.doConnect(); } function submitPassword() { wifiLogic.pendingConnect = {ssid: wifiLogic.passwordPromptSsid, security: wifiLogic.connectSecurity, password: wifiLogic.passwordInput}; - listConnectionsProcess.running = true; + wifiLogic.doConnect(); } function doConnect() { const params = wifiLogic.pendingConnect; + if (!params) return; + wifiLogic.connectingSsid = params.ssid; + + // Find the target network in our networks data + let targetNetwork = null; + for (let i = 0; i < wifiLogic.networks.length; ++i) { + if (wifiLogic.networks[i].ssid === params.ssid) { + targetNetwork = wifiLogic.networks[i]; + break; + } + } + + // Check if profile already exists using existing field + if (targetNetwork && targetNetwork.existing) { + // Profile exists, just bring it up (no password prompt) + upConnectionProcess.profileName = params.ssid; + upConnectionProcess.running = true; + wifiLogic.pendingConnect = null; + return; + } + + // No existing profile, proceed with normal connection flow if (params.security && params.security !== "--") { getInterfaceProcess.running = true; return; @@ -146,50 +204,7 @@ Item { } } - Process { - id: listConnectionsProcess - running: false - command: ["nmcli", "-t", "-f", "NAME", "connection", "show"] - stdout: StdioCollector { - onStreamFinished: { - var params = wifiLogic.pendingConnect; - var lines = text.split("\n"); - var expectedProfile = wifiLogic.profileNameForSsid(params.ssid); - var foundProfile = null; - for (var i = 0; i < lines.length; ++i) { - if (lines[i] === expectedProfile) { - foundProfile = lines[i]; - break; - } - } - if (foundProfile) { - // Profile exists, just bring it up (no password prompt) - upConnectionProcess.profileName = foundProfile; - upConnectionProcess.running = true; - } else { - // No profile: check if secured - if (wifiLogic.isSecured(params.security)) { - if (params.password && params.password.length > 0) { - // Password provided, proceed to connect - wifiLogic.doConnect(); - } else { - // No password yet, prompt for it - wifiLogic.passwordPromptSsid = params.ssid; - wifiLogic.passwordInput = ""; - wifiLogic.showPasswordPrompt = true; - wifiLogic.connectStatus = ""; - wifiLogic.connectStatusSsid = ""; - wifiLogic.connectError = ""; - wifiLogic.connectSecurity = params.security; - } - } else { - // Open, connect directly - wifiLogic.doConnect(); - } - } - } - } - } + // Handles connecting to a Wi-Fi network, with or without password Process { @@ -256,7 +271,7 @@ Item { addConnectionProcess.ifname = wifiLogic.detectedInterface; addConnectionProcess.ssid = params.ssid; addConnectionProcess.password = params.password; - addConnectionProcess.profileName = wifiLogic.profileNameForSsid(params.ssid); + addConnectionProcess.profileName = params.ssid; addConnectionProcess.security = params.security; addConnectionProcess.running = true; } else { @@ -413,8 +428,8 @@ Item { Layout.preferredWidth: 24 Layout.preferredHeight: 24 Layout.alignment: Qt.AlignVCenter - visible: scanProcess.running - running: scanProcess.running + visible: false + running: false color: Theme.accentPrimary // Assuming Spinner supports color property size: 22 // Based on the existing Spinner usage } @@ -490,8 +505,13 @@ Item { model: wifiLogic.networks delegate: Item { id: networkEntry + + required property var modelData + property var signalIcon: wifiPanel.signalIcon + width: parent.width - height: modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt ? 102 : 42 + height: (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt ? 102 : 42) + + (modelData.ssid === wifiLogic.actionPanelSsid ? 60 : 0) ColumnLayout { anchors.fill: parent spacing: 0 @@ -596,10 +616,11 @@ Item { anchors.fill: parent hoverEnabled: true onClicked: { - if (modelData.connected) { - wifiLogic.disconnectNetwork(modelData.ssid); + // Toggle the action panel for this network + if (wifiLogic.actionPanelSsid === modelData.ssid) { + wifiLogic.actionPanelSsid = ""; // Close if already open } else { - wifiLogic.connectNetwork(modelData.ssid, modelData.security); + wifiLogic.actionPanelSsid = modelData.ssid; // Open for this network } } } @@ -610,8 +631,9 @@ Item { Layout.preferredHeight: 60 radius: 8 color: "transparent" - anchors.leftMargin: 32 - anchors.rightMargin: 32 + Layout.alignment: Qt.AlignLeft + Layout.leftMargin: 32 + Layout.rightMargin: 32 z: 2 RowLayout { anchors.fill: parent @@ -651,8 +673,8 @@ Item { } } Rectangle { - width: 80 - height: 36 + Layout.preferredWidth: 80 + Layout.preferredHeight: 36 radius: 18 color: Theme.accentPrimary border.color: Theme.accentPrimary @@ -677,6 +699,102 @@ Item { } } } + // Action panel for network connection controls + Rectangle { + visible: modelData.ssid === wifiLogic.actionPanelSsid + Layout.fillWidth: true + Layout.preferredHeight: 60 + radius: 8 + color: "transparent" + Layout.alignment: Qt.AlignLeft + Layout.leftMargin: 32 + Layout.rightMargin: 32 + z: 2 + RowLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 10 + // Password field for new secured networks + Item { + Layout.fillWidth: true + Layout.preferredHeight: 36 + visible: wifiLogic.isSecured(modelData.security) && !modelData.connected && !modelData.existing + Rectangle { + anchors.fill: parent + radius: 8 + color: "transparent" + border.color: actionPanelPasswordField.activeFocus ? Theme.accentPrimary : Theme.outline + border.width: 1 + TextInput { + id: actionPanelPasswordField + anchors.fill: parent + anchors.margins: 12 + font.pixelSize: 13 + color: Theme.textPrimary + verticalAlignment: TextInput.AlignVCenter + clip: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhNone + echoMode: TextInput.Password + onAccepted: { + // Connect with the entered password + wifiLogic.pendingConnect = {ssid: modelData.ssid, security: modelData.security, password: text}; + wifiLogic.doConnect(); + + wifiLogic.actionPanelSsid = ""; // Close the panel + } + } + } + } + // Connect/Disconnect button + Rectangle { + Layout.preferredWidth: 80 + Layout.preferredHeight: 36 + radius: 18 + color: modelData.connected ? Theme.error : Theme.accentPrimary + border.color: modelData.connected ? Theme.error : Theme.accentPrimary + border.width: 0 + opacity: 1.0 + Behavior on color { ColorAnimation { duration: 100 } } + MouseArea { + anchors.fill: parent + onClicked: { + if (modelData.connected) { + // Disconnect from network + wifiLogic.disconnectNetwork(modelData.ssid); + } else { + // For secured networks, check if we need password + if (wifiLogic.isSecured(modelData.security) && !modelData.existing) { + // If password field is visible and has content, use it + if (actionPanelPasswordField.text.length > 0) { + wifiLogic.pendingConnect = {ssid: modelData.ssid, security: modelData.security, password: actionPanelPasswordField.text}; + wifiLogic.doConnect(); + } + // For new networks without password entered, we might want to show an error or handle differently + // For now, we'll just close the panel + } else { + // Connect to open network + wifiLogic.connectNetwork(modelData.ssid, modelData.security); + } + } + wifiLogic.actionPanelSsid = ""; // Close the panel + } + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = modelData.connected ? Qt.darker(Theme.error, 1.1) : Qt.darker(Theme.accentPrimary, 1.1) + onExited: parent.color = modelData.connected ? Theme.error : Theme.accentPrimary + } + Text { + anchors.centerIn: parent + text: modelData.connected ? "wifi_off" : "check" + font.family: "Material Symbols Outlined" + font.pixelSize: 20 + color: Theme.backgroundPrimary + } + } + } + } } } } From 9347fd006dbd0dabdd18c73c799d2f55d2c4e06b Mon Sep 17 00:00:00 2001 From: JPratama7 Date: Sun, 3 Aug 2025 19:43:52 +0700 Subject: [PATCH 06/11] refactor: remove debug logging and reset network state after wifi scan --- Widgets/Sidebar/Panel/WifiPanel.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Widgets/Sidebar/Panel/WifiPanel.qml b/Widgets/Sidebar/Panel/WifiPanel.qml index 6a2083d..58956ab 100644 --- a/Widgets/Sidebar/Panel/WifiPanel.qml +++ b/Widgets/Sidebar/Panel/WifiPanel.qml @@ -106,7 +106,7 @@ Item { } } wifiLogic.networks = Object.values(networksMap); - console.log(JSON.stringify(wifiLogic.networks)); + scanProcess.existingNetwork = null; refreshIndicator.running = false; refreshIndicator.visible = false; } From 3cf3474957ada153297b7a55f8d43fcab5c39830 Mon Sep 17 00:00:00 2001 From: JPratama7 Date: Sun, 3 Aug 2025 20:56:16 +0700 Subject: [PATCH 07/11] style: format code with consistent indentation and object literals --- Widgets/Sidebar/Panel/WifiPanel.qml | 115 +++++++++++++++++++--------- 1 file changed, 78 insertions(+), 37 deletions(-) diff --git a/Widgets/Sidebar/Panel/WifiPanel.qml b/Widgets/Sidebar/Panel/WifiPanel.qml index 58956ab..6002551 100644 --- a/Widgets/Sidebar/Panel/WifiPanel.qml +++ b/Widgets/Sidebar/Panel/WifiPanel.qml @@ -11,7 +11,7 @@ import qs.Helpers Item { property alias panel: wifiPanelModal - + function showAt() { wifiPanelModal.visible = true; wifiLogic.refreshNetworks(); @@ -22,10 +22,14 @@ Item { } function signalIcon(signal) { - if (signal >= 80) return "network_wifi"; - if (signal >= 60) return "network_wifi_3_bar"; - if (signal >= 40) return "network_wifi_2_bar"; - if (signal >= 20) return "network_wifi_1_bar"; + if (signal >= 80) + return "network_wifi"; + if (signal >= 60) + return "network_wifi_3_bar"; + if (signal >= 40) + return "network_wifi_2_bar"; + if (signal >= 20) + return "network_wifi_1_bar"; return "wifi_0_bar"; } @@ -43,7 +47,8 @@ Item { for (let i = 0; i < lines.length; ++i) { const line = lines[i].trim(); - if (!line) continue; + if (!line) + continue; const parts = line.split(":"); if (parts.length < 2) { @@ -55,7 +60,10 @@ Item { const type = parts[1]; if (ssid) { - networksMap[ssid] = { ssid: ssid, type: type }; + networksMap[ssid] = { + ssid: ssid, + type: type + }; } } scanProcess.existingNetwork = networksMap; @@ -70,7 +78,7 @@ Item { command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] property var existingNetwork - + stdout: StdioCollector { onStreamFinished: { const lines = text.split("\n"); @@ -78,7 +86,8 @@ Item { for (let i = 0; i < lines.length; ++i) { const line = lines[i].trim(); - if (!line) continue; + if (!line) + continue; const parts = line.split(":"); if (parts.length < 4) { @@ -92,7 +101,13 @@ Item { if (ssid) { if (!networksMap[ssid]) { - networksMap[ssid] = { ssid: ssid, security: security, signal: signal, connected: inUse, existing: ssid in scanProcess.existingNetwork }; + networksMap[ssid] = { + ssid: ssid, + security: security, + signal: signal, + connected: inUse, + existing: ssid in scanProcess.existingNetwork + }; } else { const existingNet = networksMap[ssid]; if (inUse) { @@ -106,7 +121,7 @@ Item { } } wifiLogic.networks = Object.values(networksMap); - scanProcess.existingNetwork = null; + scanProcess.existingNetwork = {}; refreshIndicator.running = false; refreshIndicator.visible = false; } @@ -144,19 +159,28 @@ Item { wifiLogic.refreshNetworks(); } function connectNetwork(ssid, security) { - wifiLogic.pendingConnect = {ssid: ssid, security: security, password: ""}; + wifiLogic.pendingConnect = { + ssid: ssid, + security: security, + password: "" + }; wifiLogic.doConnect(); } function submitPassword() { - wifiLogic.pendingConnect = {ssid: wifiLogic.passwordPromptSsid, security: wifiLogic.connectSecurity, password: wifiLogic.passwordInput}; + wifiLogic.pendingConnect = { + ssid: wifiLogic.passwordPromptSsid, + security: wifiLogic.connectSecurity, + password: wifiLogic.passwordInput + }; wifiLogic.doConnect(); } function doConnect() { const params = wifiLogic.pendingConnect; - if (!params) return; - + if (!params) + return; + wifiLogic.connectingSsid = params.ssid; - + // Find the target network in our networks data let targetNetwork = null; for (let i = 0; i < wifiLogic.networks.length; ++i) { @@ -165,7 +189,7 @@ Item { break; } } - + // Check if profile already exists using existing field if (targetNetwork && targetNetwork.existing) { // Profile exists, just bring it up (no password prompt) @@ -174,7 +198,7 @@ Item { wifiLogic.pendingConnect = null; return; } - + // No existing profile, proceed with normal connection flow if (params.security && params.security !== "--") { getInterfaceProcess.running = true; @@ -204,8 +228,6 @@ Item { } } - - // Handles connecting to a Wi-Fi network, with or without password Process { id: connectProcess @@ -221,9 +243,9 @@ Item { } command: { if (password) { - return ["nmcli", "device", "wifi", "connect", ssid, "password", password] + return ["nmcli", "device", "wifi", "connect", ssid, "password", password]; } else { - return ["nmcli", "device", "wifi", "connect", ssid] + return ["nmcli", "device", "wifi", "connect", ssid]; } } stdout: StdioCollector { @@ -354,7 +376,8 @@ Item { // Wifi button (no background card) Rectangle { id: wifiButton - width: 36; height: 36 + width: 36 + height: 36 radius: 18 border.color: Theme.accentPrimary border.width: 1 @@ -365,9 +388,7 @@ Item { text: "wifi" font.family: "Material Symbols Outlined" font.pixelSize: 22 - color: wifiButtonArea.containsMouse - ? Theme.backgroundPrimary - : Theme.accentPrimary + color: wifiButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } @@ -393,7 +414,7 @@ Item { margins.top: 0 WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand Component.onCompleted: { - wifiLogic.refreshNetworks() + wifiLogic.refreshNetworks(); } Rectangle { anchors.fill: parent @@ -422,7 +443,9 @@ Item { color: Theme.textPrimary Layout.fillWidth: true } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } Spinner { id: refreshIndicator Layout.preferredWidth: 24 @@ -440,7 +463,9 @@ Item { } Rectangle { - implicitWidth: 36; implicitHeight: 36; radius: 18 + implicitWidth: 36 + implicitHeight: 36 + radius: 18 color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent" border.color: Theme.accentPrimary border.width: 1 @@ -510,8 +535,7 @@ Item { property var signalIcon: wifiPanel.signalIcon width: parent.width - height: (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt ? 102 : 42) + - (modelData.ssid === wifiLogic.actionPanelSsid ? 60 : 0) + height: (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt ? 102 : 42) + (modelData.ssid === wifiLogic.actionPanelSsid ? 60 : 0) ColumnLayout { anchors.fill: parent spacing: 0 @@ -548,7 +572,8 @@ Item { Layout.alignment: Qt.AlignVCenter } Item { - width: 22; height: 22 + width: 22 + height: 22 visible: wifiLogic.connectStatusSsid === modelData.ssid && wifiLogic.connectStatus !== "" RowLayout { anchors.fill: parent @@ -680,7 +705,11 @@ Item { border.color: Theme.accentPrimary border.width: 0 opacity: 1.0 - Behavior on color { ColorAnimation { duration: 100 } } + Behavior on color { + ColorAnimation { + duration: 100 + } + } MouseArea { anchors.fill: parent onClicked: wifiLogic.submitPassword() @@ -739,9 +768,13 @@ Item { echoMode: TextInput.Password onAccepted: { // Connect with the entered password - wifiLogic.pendingConnect = {ssid: modelData.ssid, security: modelData.security, password: text}; + wifiLogic.pendingConnect = { + ssid: modelData.ssid, + security: modelData.security, + password: text + }; wifiLogic.doConnect(); - + wifiLogic.actionPanelSsid = ""; // Close the panel } } @@ -756,7 +789,11 @@ Item { border.color: modelData.connected ? Theme.error : Theme.accentPrimary border.width: 0 opacity: 1.0 - Behavior on color { ColorAnimation { duration: 100 } } + Behavior on color { + ColorAnimation { + duration: 100 + } + } MouseArea { anchors.fill: parent onClicked: { @@ -768,7 +805,11 @@ Item { if (wifiLogic.isSecured(modelData.security) && !modelData.existing) { // If password field is visible and has content, use it if (actionPanelPasswordField.text.length > 0) { - wifiLogic.pendingConnect = {ssid: modelData.ssid, security: modelData.security, password: actionPanelPasswordField.text}; + wifiLogic.pendingConnect = { + ssid: modelData.ssid, + security: modelData.security, + password: actionPanelPasswordField.text + }; wifiLogic.doConnect(); } // For new networks without password entered, we might want to show an error or handle differently From 90e3deb589a9cad3fd4616f267e545c6d995fd81 Mon Sep 17 00:00:00 2001 From: JPratama7 Date: Sun, 3 Aug 2025 21:27:49 +0700 Subject: [PATCH 08/11] refactor: optimize wifi network lookup by using map instead of array iteration, change cli command for connect --- Widgets/Sidebar/Panel/WifiPanel.qml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Widgets/Sidebar/Panel/WifiPanel.qml b/Widgets/Sidebar/Panel/WifiPanel.qml index 6002551..4fc5b11 100644 --- a/Widgets/Sidebar/Panel/WifiPanel.qml +++ b/Widgets/Sidebar/Panel/WifiPanel.qml @@ -120,7 +120,7 @@ Item { } } } - wifiLogic.networks = Object.values(networksMap); + wifiLogic.networks = networksMap; scanProcess.existingNetwork = {}; refreshIndicator.running = false; refreshIndicator.visible = false; @@ -182,13 +182,7 @@ Item { wifiLogic.connectingSsid = params.ssid; // Find the target network in our networks data - let targetNetwork = null; - for (let i = 0; i < wifiLogic.networks.length; ++i) { - if (wifiLogic.networks[i].ssid === params.ssid) { - targetNetwork = wifiLogic.networks[i]; - break; - } - } + const targetNetwork = wifiLogic.networks[params.ssid]; // Check if profile already exists using existing field if (targetNetwork && targetNetwork.existing) { @@ -220,7 +214,7 @@ Item { id: disconnectProfileProcess property string connectionName: "" running: false - command: ["nmcli", "connection", "down", "id", connectionName] + command: ["nmcli", "connection", "down", connectionName] onRunningChanged: { if (!running) { wifiLogic.refreshNetworks(); @@ -527,7 +521,7 @@ Item { anchors.fill: parent spacing: 4 boundsBehavior: Flickable.StopAtBounds - model: wifiLogic.networks + model: Object.values(wifiLogic.networks) delegate: Item { id: networkEntry From a2b4f4b6b62f636743300a13e914bacf9c79c2d7 Mon Sep 17 00:00:00 2001 From: JPratama7 Date: Sun, 3 Aug 2025 22:09:08 +0700 Subject: [PATCH 09/11] feat: remove quickshell prefix for profilename --- Widgets/Sidebar/Panel/WifiPanel.qml | 73 +++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/Widgets/Sidebar/Panel/WifiPanel.qml b/Widgets/Sidebar/Panel/WifiPanel.qml index 4fc5b11..b02737b 100644 --- a/Widgets/Sidebar/Panel/WifiPanel.qml +++ b/Widgets/Sidebar/Panel/WifiPanel.qml @@ -56,7 +56,7 @@ Item { continue; } - const ssid = parts[0]; + const ssid = wifiLogic.replaceQuickshell(parts[0]); const type = parts[1]; if (ssid) { @@ -120,6 +120,8 @@ Item { } } } + + wifiLogic.networks = networksMap; scanProcess.existingNetwork = {}; refreshIndicator.running = false; @@ -130,7 +132,7 @@ Item { QtObject { id: wifiLogic - property var networks: [] + property var networks: {} property var anchorItem: null property real anchorX property real anchorY @@ -146,6 +148,28 @@ Item { property string detectedInterface: "" property string actionPanelSsid: "" + function replaceQuickshell(ssid: string): string { + const newName = ssid.replace("quickshell-", ""); + + if (!ssid.startsWith("quickshell-")){ + return newName; + } + + if (newName in wifiLogic.networks){ + console.log(`Quickshell ${newName} already exists, deleting old profile`) + deleteProfileProcess.connName = ssid; + deleteProfileProcess.running = true; + } + + + console.log(`Changing from ${ssid} to ${newName}`) + renameConnectionProcess.oldName = ssid; + renameConnectionProcess.newName = newName; + renameConnectionProcess.running = true; + + return newName; + } + function disconnectNetwork(ssid) { const profileName = ssid; disconnectProfileProcess.connectionName = profileName; @@ -222,6 +246,47 @@ Item { } } + // Process to rename a connection + Process { + id: renameConnectionProcess + running: false + property string oldName: "" + property string newName: "" + command: ["nmcli", "connection", "modify", oldName, "connection.id", newName] + + stdout: StdioCollector { + onStreamFinished: { + console.log("Renamed connection '" + renameConnectionProcess.oldName + "' to '" + renameConnectionProcess.newName + "'"); + } + } + stderr: StdioCollector { + onStreamFinished: { + console.error("Error renaming connection '" + renameConnectionProcess.oldName + "':", text); + } + } + } + + + // Process to rename a connection + Process { + id: deleteProfileProcess + running: false + property string connName: "" + command: ["nmcli", "connection", "delete", `'${connName}'`] + + stdout: StdioCollector { + onStreamFinished: { + console.log("Deleted connection '" + deleteProfileProcess.connName + "'"); + } + } + stderr: StdioCollector { + onStreamFinished: { + console.error("Error deleting connection '" + deleteProfileProcess.connName + "':", text); + } + } + } + + // Handles connecting to a Wi-Fi network, with or without password Process { id: connectProcess @@ -237,9 +302,9 @@ Item { } command: { if (password) { - return ["nmcli", "device", "wifi", "connect", ssid, "password", password]; + return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password]; } else { - return ["nmcli", "device", "wifi", "connect", ssid]; + return ["nmcli", "device", "wifi", "connect", `'${ssid}'`]; } } stdout: StdioCollector { From 9fce72f05497698bca6b4c85fe951ed7c3aa9302 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 3 Aug 2025 18:08:48 +0200 Subject: [PATCH 10/11] Edit WifiPanel.qml --- Widgets/Sidebar/Panel/WifiPanel.qml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Widgets/Sidebar/Panel/WifiPanel.qml b/Widgets/Sidebar/Panel/WifiPanel.qml index b02737b..fc88e0c 100644 --- a/Widgets/Sidebar/Panel/WifiPanel.qml +++ b/Widgets/Sidebar/Panel/WifiPanel.qml @@ -151,17 +151,16 @@ Item { function replaceQuickshell(ssid: string): string { const newName = ssid.replace("quickshell-", ""); - if (!ssid.startsWith("quickshell-")){ + if (!ssid.startsWith("quickshell-")) { return newName; } - if (newName in wifiLogic.networks){ + if (wifiLogic.networks && newName in wifiLogic.networks) { console.log(`Quickshell ${newName} already exists, deleting old profile`) deleteProfileProcess.connName = ssid; deleteProfileProcess.running = true; } - console.log(`Changing from ${ssid} to ${newName}`) renameConnectionProcess.oldName = ssid; renameConnectionProcess.newName = newName; @@ -256,17 +255,22 @@ Item { stdout: StdioCollector { onStreamFinished: { - console.log("Renamed connection '" + renameConnectionProcess.oldName + "' to '" + renameConnectionProcess.newName + "'"); + console.log("Successfully renamed connection '" + + renameConnectionProcess.oldName + "' to '" + + renameConnectionProcess.newName + "'"); } } stderr: StdioCollector { onStreamFinished: { - console.error("Error renaming connection '" + renameConnectionProcess.oldName + "':", text); + if (text.trim() !== "" && !text.toLowerCase().includes("warning")) { + console.error("Error renaming connection:", text); + } } } } + // Process to rename a connection Process { id: deleteProfileProcess From bb8b552e366aebfbc5d6e5267ac8ec2710f015a1 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 3 Aug 2025 18:23:55 +0200 Subject: [PATCH 11/11] Fix tray submenus --- Bar/Modules/CustomTrayMenu.qml | 477 ++++++++++++++++++++++++++++----- Bar/Modules/SystemTray.qml | 1 - 2 files changed, 406 insertions(+), 72 deletions(-) diff --git a/Bar/Modules/CustomTrayMenu.qml b/Bar/Modules/CustomTrayMenu.qml index 9d897f0..a64ac7b 100644 --- a/Bar/Modules/CustomTrayMenu.qml +++ b/Bar/Modules/CustomTrayMenu.qml @@ -1,9 +1,8 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 import Quickshell -import Quickshell.Widgets import qs.Settings PopupWindow { @@ -22,120 +21,456 @@ PopupWindow { anchor.rect.x: anchorX anchor.rect.y: anchorY - 4 + // Recursive function to destroy all open submenus in delegate tree, safely avoiding infinite recursion + function destroySubmenusRecursively(item) { + if (!item || !item.contentItem) return; + var children = item.contentItem.children; + for (var i = 0; i < children.length; ++i) { + var child = children[i]; + if (child.subMenu) { + child.subMenu.hideMenu(); + child.subMenu.destroy(); + child.subMenu = null; + } + // Recursively destroy submenus only if the child has contentItem to prevent issues + if (child.contentItem) { + destroySubmenusRecursively(child); + } + } + } + function showAt(item, x, y) { if (!item) { - console.warn("CustomTrayMenu: anchorItem is undefined, not showing menu."); + console.warn("CustomTrayMenu: anchorItem is undefined, won't show menu."); return; } - anchorItem = item - anchorX = x - anchorY = y - visible = true - forceActiveFocus() - Qt.callLater(() => trayMenu.anchor.updateAnchor()) + anchorItem = item; + anchorX = x; + anchorY = y; + visible = true; + forceActiveFocus(); + Qt.callLater(() => trayMenu.anchor.updateAnchor()); } function hideMenu() { - visible = false + visible = false; + destroySubmenusRecursively(listView); } Item { - anchors.fill: parent - Keys.onEscapePressed: trayMenu.hideMenu() + anchors.fill: parent; + Keys.onEscapePressed: trayMenu.hideMenu(); } QsMenuOpener { - id: opener - menu: trayMenu.menu + id: opener; + menu: trayMenu.menu; } Rectangle { - id: bg - anchors.fill: parent - color: Theme.surfaceVariant || "#222" - border.color: Theme.outline || "#444" - border.width: 1 - radius: 12 - z: 0 + id: bg; + anchors.fill: parent; + color: Theme.backgroundPrimary || "#222"; + border.color: Theme.outline || "#444"; + border.width: 1; + radius: 12; + z: 0; } ListView { - id: listView - anchors.fill: parent - anchors.margins: 6 - spacing: 2 - interactive: false - enabled: trayMenu.visible - clip: true + id: listView; + anchors.fill: parent; + anchors.margins: 6; + spacing: 2; + interactive: false; + enabled: trayMenu.visible; + clip: true; model: ScriptModel { values: opener.children ? [...opener.children.values] : [] } delegate: Rectangle { - id: entry - required property var modelData + id: entry; + required property var modelData; - width: listView.width - height: (modelData?.isSeparator) ? 8 : 32 - color: "transparent" - radius: 12 + width: listView.width; + height: (modelData?.isSeparator) ? 8 : 32; + color: "transparent"; + radius: 12; + + property var subMenu: null; Rectangle { - anchors.centerIn: parent - width: parent.width - 20 - height: 1 - color: Qt.darker(Theme.surfaceVariant || "#222", 1.4) - visible: modelData?.isSeparator ?? false + anchors.centerIn: parent; + width: parent.width - 20; + height: 1; + color: Qt.darker(Theme.backgroundPrimary || "#222", 1.4); + visible: modelData?.isSeparator ?? false; } Rectangle { - id: bg - anchors.fill: parent - color: mouseArea.containsMouse ? Theme.highlight : "transparent" - radius: 8 - visible: !(modelData?.isSeparator ?? false) - property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary + id: bg; + anchors.fill: parent; + color: mouseArea.containsMouse ? Theme.highlight : "transparent"; + radius: 8; + visible: !(modelData?.isSeparator ?? false); + property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary; RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 8 + anchors.fill: parent; + anchors.leftMargin: 12; + anchors.rightMargin: 12; + spacing: 8; Text { - Layout.fillWidth: true - color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled - text: modelData?.text ?? "" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight + Layout.fillWidth: true; + color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled; + text: modelData?.text ?? ""; + font.family: Theme.fontFamily; + font.pixelSize: Theme.fontSizeSmall; + verticalAlignment: Text.AlignVCenter; + elide: Text.ElideRight; } - IconImage { - Layout.preferredWidth: 16 - Layout.preferredHeight: 16 - source: modelData?.icon ?? "" - visible: (modelData?.icon ?? "") !== "" - backer.fillMode: Image.PreserveAspectFit + Image { + Layout.preferredWidth: 16; + Layout.preferredHeight: 16; + source: modelData?.icon ?? ""; + visible: (modelData?.icon ?? "") !== ""; + fillMode: Image.PreserveAspectFit; + } + + Text { + // Material Symbols Outlined chevron right for submenu + text: modelData?.hasChildren ? "menu" : ""; + font.family: "Material Symbols Outlined"; + font.pixelSize: 18; + verticalAlignment: Text.AlignVCenter; + visible: modelData?.hasChildren ?? false; + color: Theme.textPrimary; } } MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible + id: mouseArea; + anchors.fill: parent; + hoverEnabled: true; + enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible; onClicked: { if (modelData && !modelData.isSeparator) { - modelData.triggered() - trayMenu.hideMenu() + if (modelData.hasChildren) { + // Submenus open on hover; ignore click here + return; + } + modelData.triggered(); + trayMenu.hideMenu(); + } + } + + onEntered: { + if (!trayMenu.visible) return; + + if (modelData?.hasChildren) { + // Close sibling submenus immediately + for (let i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i]; + if (sibling !== entry && sibling.subMenu) { + sibling.subMenu.hideMenu(); + sibling.subMenu.destroy(); + sibling.subMenu = null; + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } + var globalPos = entry.mapToGlobal(0, 0); + var submenuWidth = 180; + var gap = 12; + var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width); + var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap; + + entry.subMenu = subMenuComponent.createObject(trayMenu, { + menu: modelData, + anchorItem: entry, + anchorX: anchorX, + anchorY: 0 + }); + entry.subMenu.showAt(entry, anchorX, 0); + } else { + // Hovered item without submenu; close siblings + for (let i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i]; + if (sibling.subMenu) { + sibling.subMenu.hideMenu(); + sibling.subMenu.destroy(); + sibling.subMenu = null; + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } + } + } + + onExited: { + if (entry.subMenu && !entry.subMenu.containsMouse()) { + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } + } + } + } + + // Simplified containsMouse without recursive calls to avoid stack overflow + function containsMouse() { + return mouseArea.containsMouse; + } + + Component.onDestruction: { + if (subMenu) { + subMenu.destroy(); + subMenu = null; + } + } + } + } + + Component { + id: subMenuComponent; + + PopupWindow { + id: subMenu; + implicitWidth: 180; + implicitHeight: Math.max(40, listView.contentHeight + 12); + visible: false; + color: "transparent"; + + property QsMenuHandle menu; + property var anchorItem: null; + property real anchorX; + property real anchorY; + + anchor.item: anchorItem ? anchorItem : null; + anchor.rect.x: anchorX; + anchor.rect.y: anchorY; + + function showAt(item, x, y) { + if (!item) { + console.warn("subMenuComponent: anchorItem is undefined, not showing menu."); + return; + } + anchorItem = item; + anchorX = x; + anchorY = y; + visible = true; + Qt.callLater(() => subMenu.anchor.updateAnchor()); + } + + function hideMenu() { + visible = false; + // Close all submenus recursively in this submenu + for (let i = 0; i < listView.contentItem.children.length; i++) { + const child = listView.contentItem.children[i]; + if (child.subMenu) { + child.subMenu.hideMenu(); + child.subMenu.destroy(); + child.subMenu = null; + } + } + } + + // Simplified containsMouse avoiding recursive calls + function containsMouse() { + return subMenu.containsMouse; + } + + Item { + anchors.fill: parent; + Keys.onEscapePressed: subMenu.hideMenu(); + } + + QsMenuOpener { + id: opener; + menu: subMenu.menu; + } + + Rectangle { + id: bg; + anchors.fill: parent; + color: Theme.backgroundPrimary || "#222"; + border.color: Theme.outline || "#444"; + border.width: 1; + radius: 12; + z: 0; + } + + ListView { + id: listView; + anchors.fill: parent; + anchors.margins: 6; + spacing: 2; + interactive: false; + enabled: subMenu.visible; + clip: true; + + model: ScriptModel { + values: opener.children ? [...opener.children.values] : []; + } + + delegate: Rectangle { + id: entry; + required property var modelData; + + width: listView.width; + height: (modelData?.isSeparator) ? 8 : 32; + color: "transparent"; + radius: 12; + + property var subMenu: null; + + Rectangle { + anchors.centerIn: parent; + width: parent.width - 20; + height: 1; + color: Qt.darker(Theme.surfaceVariant || "#222", 1.4); + visible: modelData?.isSeparator ?? false; + } + + Rectangle { + id: bg; + anchors.fill: parent; + color: mouseArea.containsMouse ? Theme.highlight : "transparent"; + radius: 8; + visible: !(modelData?.isSeparator ?? false); + property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary; + + RowLayout { + anchors.fill: parent; + anchors.leftMargin: 12; + anchors.rightMargin: 12; + spacing: 8; + + Text { + Layout.fillWidth: true; + color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled; + text: modelData?.text ?? ""; + font.family: Theme.fontFamily; + font.pixelSize: Theme.fontSizeSmall; + verticalAlignment: Text.AlignVCenter; + elide: Text.ElideRight; + } + + Image { + Layout.preferredWidth: 16; + Layout.preferredHeight: 16; + source: modelData?.icon ?? ""; + visible: (modelData?.icon ?? "") !== ""; + fillMode: Image.PreserveAspectFit; + } + + Text { + text: modelData?.hasChildren ? "\uE5CC" : ""; + font.family: "Material Symbols Outlined"; + font.pixelSize: 18; + verticalAlignment: Text.AlignVCenter; + visible: modelData?.hasChildren ?? false; + color: Theme.textPrimary; + } + } + + MouseArea { + id: mouseArea; + anchors.fill: parent; + hoverEnabled: true; + enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && subMenu.visible; + + onClicked: { + if (modelData && !modelData.isSeparator) { + if (modelData.hasChildren) { + return; + } + modelData.triggered(); + trayMenu.hideMenu(); + } + } + + onEntered: { + if (!subMenu.visible) return; + + if (modelData?.hasChildren) { + for (let i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i]; + if (sibling !== entry && sibling.subMenu) { + sibling.subMenu.hideMenu(); + sibling.subMenu.destroy(); + sibling.subMenu = null; + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } + var globalPos = entry.mapToGlobal(0, 0); + var submenuWidth = 180; + var gap = 12; + var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width); + var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap; + + entry.subMenu = subMenuComponent.createObject(subMenu, { + menu: modelData, + anchorItem: entry, + anchorX: anchorX, + anchorY: 0 + }); + entry.subMenu.showAt(entry, anchorX, 0); + } else { + for (let i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i]; + if (sibling.subMenu) { + sibling.subMenu.hideMenu(); + sibling.subMenu.destroy(); + sibling.subMenu = null; + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } + } + } + + onExited: { + if (entry.subMenu && !entry.subMenu.containsMouse()) { + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } + } + } + } + + // Simplified & safe containsMouse avoiding recursion + function containsMouse() { + return mouseArea.containsMouse; + } + + Component.onDestruction: { + if (subMenu) { + subMenu.destroy(); + subMenu = null; } } } } } } -} \ No newline at end of file +} diff --git a/Bar/Modules/SystemTray.qml b/Bar/Modules/SystemTray.qml index fe2ceee..0217298 100644 --- a/Bar/Modules/SystemTray.qml +++ b/Bar/Modules/SystemTray.qml @@ -113,7 +113,6 @@ Row { modelData.secondaryActivate && modelData.secondaryActivate() } else if (mouse.button === Qt.RightButton) { trayTooltip.tooltipVisible = false - console.log("Right click on", modelData.id, "hasMenu:", modelData.hasMenu, "menu:", modelData.menu) // If menu is already visible, close it if (trayMenu && trayMenu.visible) { trayMenu.hideMenu()