Merge branch 'main' into remove-qt5-dep

This commit is contained in:
leiserfg 2025-08-03 19:37:45 +02:00 committed by GitHub
commit c4ed39e6a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 777 additions and 196 deletions

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
.qmlls.ini .qmlls.ini
notification_history.json notification_history.json
Programs

View file

@ -1,9 +1,8 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick 2.15
import QtQuick.Controls import QtQuick.Controls 2.15
import QtQuick.Layouts import QtQuick.Layouts 1.15
import Quickshell import Quickshell
import Quickshell.Widgets
import qs.Settings import qs.Settings
PopupWindow { PopupWindow {
@ -22,120 +21,456 @@ PopupWindow {
anchor.rect.x: anchorX anchor.rect.x: anchorX
anchor.rect.y: anchorY - 4 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) { function showAt(item, x, y) {
if (!item) { if (!item) {
console.warn("CustomTrayMenu: anchorItem is undefined, not showing menu."); console.warn("CustomTrayMenu: anchorItem is undefined, won't show menu.");
return; return;
} }
anchorItem = item anchorItem = item;
anchorX = x anchorX = x;
anchorY = y anchorY = y;
visible = true visible = true;
forceActiveFocus() forceActiveFocus();
Qt.callLater(() => trayMenu.anchor.updateAnchor()) Qt.callLater(() => trayMenu.anchor.updateAnchor());
} }
function hideMenu() { function hideMenu() {
visible = false visible = false;
destroySubmenusRecursively(listView);
} }
Item { Item {
anchors.fill: parent anchors.fill: parent;
Keys.onEscapePressed: trayMenu.hideMenu() Keys.onEscapePressed: trayMenu.hideMenu();
} }
QsMenuOpener { QsMenuOpener {
id: opener id: opener;
menu: trayMenu.menu menu: trayMenu.menu;
} }
Rectangle { Rectangle {
id: bg id: bg;
anchors.fill: parent anchors.fill: parent;
color: Theme.surfaceVariant || "#222" color: Theme.backgroundPrimary || "#222";
border.color: Theme.outline || "#444" border.color: Theme.outline || "#444";
border.width: 1 border.width: 1;
radius: 12 radius: 12;
z: 0 z: 0;
} }
ListView { ListView {
id: listView id: listView;
anchors.fill: parent anchors.fill: parent;
anchors.margins: 6 anchors.margins: 6;
spacing: 2 spacing: 2;
interactive: false interactive: false;
enabled: trayMenu.visible enabled: trayMenu.visible;
clip: true clip: true;
model: ScriptModel { model: ScriptModel {
values: opener.children ? [...opener.children.values] : [] values: opener.children ? [...opener.children.values] : []
} }
delegate: Rectangle { delegate: Rectangle {
id: entry id: entry;
required property var modelData required property var modelData;
width: listView.width width: listView.width;
height: (modelData?.isSeparator) ? 8 : 32 height: (modelData?.isSeparator) ? 8 : 32;
color: "transparent" color: "transparent";
radius: 12 radius: 12;
property var subMenu: null;
Rectangle { Rectangle {
anchors.centerIn: parent anchors.centerIn: parent;
width: parent.width - 20 width: parent.width - 20;
height: 1 height: 1;
color: Qt.darker(Theme.surfaceVariant || "#222", 1.4) color: Qt.darker(Theme.backgroundPrimary || "#222", 1.4);
visible: modelData?.isSeparator ?? false visible: modelData?.isSeparator ?? false;
} }
Rectangle { Rectangle {
id: bg id: bg;
anchors.fill: parent anchors.fill: parent;
color: mouseArea.containsMouse ? Theme.highlight : "transparent" color: mouseArea.containsMouse ? Theme.highlight : "transparent";
radius: 8 radius: 8;
visible: !(modelData?.isSeparator ?? false) visible: !(modelData?.isSeparator ?? false);
property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary property color hoverTextColor: mouseArea.containsMouse ? Theme.onAccent : Theme.textPrimary;
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent;
anchors.leftMargin: 12 anchors.leftMargin: 12;
anchors.rightMargin: 12 anchors.rightMargin: 12;
spacing: 8 spacing: 8;
Text { Text {
Layout.fillWidth: true Layout.fillWidth: true;
color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Theme.textDisabled;
text: modelData?.text ?? "" text: modelData?.text ?? "";
font.family: Theme.fontFamily font.family: Theme.fontFamily;
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall;
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter;
elide: Text.ElideRight elide: Text.ElideRight;
} }
IconImage { Image {
Layout.preferredWidth: 16 Layout.preferredWidth: 16;
Layout.preferredHeight: 16 Layout.preferredHeight: 16;
source: modelData?.icon ?? "" source: modelData?.icon ?? "";
visible: (modelData?.icon ?? "") !== "" visible: (modelData?.icon ?? "") !== "";
backer.fillMode: Image.PreserveAspectFit 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 { MouseArea {
id: mouseArea id: mouseArea;
anchors.fill: parent anchors.fill: parent;
hoverEnabled: true hoverEnabled: true;
enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible;
onClicked: { onClicked: {
if (modelData && !modelData.isSeparator) { if (modelData && !modelData.isSeparator) {
modelData.triggered() if (modelData.hasChildren) {
trayMenu.hideMenu() // 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;
} }
} }
} }
} }
} }
} }
} }

View file

@ -113,7 +113,6 @@ Row {
modelData.secondaryActivate && modelData.secondaryActivate(); modelData.secondaryActivate && modelData.secondaryActivate();
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
trayTooltip.tooltipVisible = false; trayTooltip.tooltipVisible = false;
console.log("Right click on", modelData.id, "hasMenu:", modelData.hasMenu, "menu:", modelData.menu);
// If menu is already visible, close it // If menu is already visible, close it
if (trayMenu && trayMenu.visible) { if (trayMenu && trayMenu.visible) {
trayMenu.hideMenu(); trayMenu.hideMenu();

View file

@ -11,72 +11,128 @@ import qs.Helpers
Item { Item {
property alias panel: wifiPanelModal property alias panel: wifiPanelModal
function showAt() { function showAt() {
wifiPanelModal.visible = true; wifiPanelModal.visible = true;
wifiLogic.refreshNetworks(); wifiLogic.refreshNetworks();
} }
Component.onCompleted: {
existingNetwork.running = true;
}
function signalIcon(signal) { function signalIcon(signal) {
if (signal >= 80) return "network_wifi"; if (signal >= 80)
if (signal >= 60) return "network_wifi_3_bar"; return "network_wifi";
if (signal >= 40) return "network_wifi_2_bar"; if (signal >= 60)
if (signal >= 20) return "network_wifi_1_bar"; 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"; 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 = wifiLogic.replaceQuickshell(parts[0]);
const type = parts[1];
if (ssid) {
networksMap[ssid] = {
ssid: ssid,
type: type
};
}
}
scanProcess.existingNetwork = networksMap;
scanProcess.running = true;
}
}
}
Process { Process {
id: scanProcess id: scanProcess
running: false running: false
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
onRunningChanged: {
// Removed debug log property var existingNetwork
}
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
var lines = text.split("\n"); const lines = text.split("\n");
var nets = []; const networksMap = {};
var seen = {};
for (var i = 0; i < lines.length; ++i) { for (let i = 0; i < lines.length; ++i) {
var line = lines[i].trim(); const line = lines[i].trim();
if (!line) continue; if (!line)
var parts = line.split(":"); continue;
var ssid = parts[0];
var security = parts[1]; const parts = line.split(":");
var signal = parseInt(parts[2]); if (parts.length < 4) {
var inUse = parts[3] === "*"; 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 (ssid) {
if (!seen[ssid]) { if (!networksMap[ssid]) {
// First time seeing this SSID networksMap[ssid] = {
nets.push({ ssid: ssid, security: security, signal: signal, connected: inUse }); ssid: ssid,
seen[ssid] = true; security: security,
signal: signal,
connected: inUse,
existing: ssid in scanProcess.existingNetwork
};
} else { } else {
// SSID already exists, update if this entry has better signal or is connected const existingNet = networksMap[ssid];
for (var j = 0; j < nets.length; ++j) { if (inUse) {
if (nets[j].ssid === ssid) { existingNet.connected = true;
// Update connection status if this entry is connected }
if (inUse) { if (signal > existingNet.signal) {
nets[j].connected = true; existingNet.signal = signal;
} existingNet.security = security;
// Update signal if this entry has better signal
if (signal > nets[j].signal) {
nets[j].signal = signal;
nets[j].security = security;
}
break;
}
} }
} }
} }
} }
wifiLogic.networks = nets;
wifiLogic.networks = networksMap;
scanProcess.existingNetwork = {};
refreshIndicator.running = false;
refreshIndicator.visible = false;
} }
} }
} }
QtObject { QtObject {
id: wifiLogic id: wifiLogic
property var networks: [] property var networks: {}
property var anchorItem: null property var anchorItem: null
property real anchorX property real anchorX
property real anchorY property real anchorY
@ -90,42 +146,86 @@ Item {
property string connectSecurity: "" property string connectSecurity: ""
property var pendingConnect: null property var pendingConnect: null
property string detectedInterface: "" property string detectedInterface: ""
property string actionPanelSsid: ""
function profileNameForSsid(ssid) { function replaceQuickshell(ssid: string): string {
return "quickshell-" + ssid.replace(/[^a-zA-Z0-9]/g, "_"); const newName = ssid.replace("quickshell-", "");
if (!ssid.startsWith("quickshell-")) {
return newName;
}
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;
renameConnectionProcess.running = true;
return newName;
} }
function disconnectNetwork(ssid) { function disconnectNetwork(ssid) {
var profileName = wifiLogic.profileNameForSsid(ssid); const profileName = ssid;
disconnectProfileProcess.connectionName = profileName; disconnectProfileProcess.connectionName = profileName;
disconnectProfileProcess.running = true; disconnectProfileProcess.running = true;
} }
function refreshNetworks() { function refreshNetworks() {
scanProcess.running = true; existingNetwork.running = true;
} }
function showAt() { function showAt() {
wifiPanelModal.visible = true; wifiPanelModal.visible = true;
wifiLogic.refreshNetworks(); wifiLogic.refreshNetworks();
} }
function connectNetwork(ssid, security) { function connectNetwork(ssid, security) {
wifiLogic.pendingConnect = {ssid: ssid, security: security, password: ""}; wifiLogic.pendingConnect = {
listConnectionsProcess.running = true; ssid: ssid,
security: security,
password: ""
};
wifiLogic.doConnect();
} }
function submitPassword() { function submitPassword() {
wifiLogic.pendingConnect = {ssid: wifiLogic.passwordPromptSsid, security: wifiLogic.connectSecurity, password: wifiLogic.passwordInput}; wifiLogic.pendingConnect = {
listConnectionsProcess.running = true; ssid: wifiLogic.passwordPromptSsid,
security: wifiLogic.connectSecurity,
password: wifiLogic.passwordInput
};
wifiLogic.doConnect();
} }
function doConnect() { function doConnect() {
var params = wifiLogic.pendingConnect; const params = wifiLogic.pendingConnect;
if (!params)
return;
wifiLogic.connectingSsid = params.ssid; wifiLogic.connectingSsid = params.ssid;
// Find the target network in our networks data
const targetNetwork = wifiLogic.networks[params.ssid];
// 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 !== "--") { if (params.security && params.security !== "--") {
getInterfaceProcess.running = true; getInterfaceProcess.running = true;
} else { return;
connectProcess.security = params.security;
connectProcess.ssid = params.ssid;
connectProcess.password = params.password;
connectProcess.running = true;
wifiLogic.pendingConnect = null;
} }
connectProcess.security = params.security;
connectProcess.ssid = params.ssid;
connectProcess.password = params.password;
connectProcess.running = true;
wifiLogic.pendingConnect = null;
} }
function isSecured(security) { function isSecured(security) {
return security && security.trim() !== "" && security.trim() !== "--"; return security && security.trim() !== "" && security.trim() !== "--";
@ -137,7 +237,7 @@ Item {
id: disconnectProfileProcess id: disconnectProfileProcess
property string connectionName: "" property string connectionName: ""
running: false running: false
command: ["nmcli", "connection", "down", "id", connectionName] command: ["nmcli", "connection", "down", connectionName]
onRunningChanged: { onRunningChanged: {
if (!running) { if (!running) {
wifiLogic.refreshNetworks(); wifiLogic.refreshNetworks();
@ -145,51 +245,52 @@ Item {
} }
} }
// Process to rename a connection
Process { Process {
id: listConnectionsProcess id: renameConnectionProcess
running: false running: false
command: ["nmcli", "-t", "-f", "NAME", "connection", "show"] property string oldName: ""
property string newName: ""
command: ["nmcli", "connection", "modify", oldName, "connection.id", newName]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
var params = wifiLogic.pendingConnect; console.log("Successfully renamed connection '" +
var lines = text.split("\n"); renameConnectionProcess.oldName + "' to '" +
var expectedProfile = wifiLogic.profileNameForSsid(params.ssid); renameConnectionProcess.newName + "'");
var foundProfile = null; }
for (var i = 0; i < lines.length; ++i) { }
if (lines[i] === expectedProfile) { stderr: StdioCollector {
foundProfile = lines[i]; onStreamFinished: {
break; if (text.trim() !== "" && !text.toLowerCase().includes("warning")) {
} console.error("Error renaming connection:", text);
}
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();
}
} }
} }
} }
} }
// 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 // Handles connecting to a Wi-Fi network, with or without password
Process { Process {
id: connectProcess id: connectProcess
@ -197,11 +298,17 @@ Item {
property string password: "" property string password: ""
property string security: "" property string security: ""
running: false running: false
onStarted: {
refreshIndicator.running = true;
}
onExited: (exitCode, exitStatus) => {
refreshIndicator.running = false;
}
command: { command: {
if (password) { if (password) {
return ["nmcli", "device", "wifi", "connect", ssid, "password", password] return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password];
} else { } else {
return ["nmcli", "device", "wifi", "connect", ssid] return ["nmcli", "device", "wifi", "connect", `'${ssid}'`];
} }
} }
stdout: StdioCollector { stdout: StdioCollector {
@ -249,7 +356,7 @@ Item {
addConnectionProcess.ifname = wifiLogic.detectedInterface; addConnectionProcess.ifname = wifiLogic.detectedInterface;
addConnectionProcess.ssid = params.ssid; addConnectionProcess.ssid = params.ssid;
addConnectionProcess.password = params.password; addConnectionProcess.password = params.password;
addConnectionProcess.profileName = wifiLogic.profileNameForSsid(params.ssid); addConnectionProcess.profileName = params.ssid;
addConnectionProcess.security = params.security; addConnectionProcess.security = params.security;
addConnectionProcess.running = true; addConnectionProcess.running = true;
} else { } else {
@ -332,7 +439,8 @@ Item {
// Wifi button (no background card) // Wifi button (no background card)
Rectangle { Rectangle {
id: wifiButton id: wifiButton
width: 36; height: 36 width: 36
height: 36
radius: 18 radius: 18
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1
@ -343,9 +451,7 @@ Item {
text: "wifi" text: "wifi"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pixelSize: 22 font.pixelSize: 22
color: wifiButtonArea.containsMouse color: wifiButtonArea.containsMouse ? Theme.backgroundPrimary : Theme.accentPrimary
? Theme.backgroundPrimary
: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
@ -371,7 +477,7 @@ Item {
margins.top: 0 margins.top: 0
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Component.onCompleted: { Component.onCompleted: {
wifiLogic.refreshNetworks() wifiLogic.refreshNetworks();
} }
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@ -400,8 +506,29 @@ Item {
color: Theme.textPrimary color: Theme.textPrimary
Layout.fillWidth: true Layout.fillWidth: true
} }
Item {
Layout.fillWidth: true
}
Spinner {
id: refreshIndicator
Layout.preferredWidth: 24
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignVCenter
visible: false
running: false
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 { Rectangle {
width: 36; height: 36; radius: 18 implicitWidth: 36
implicitHeight: 36
radius: 18
color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent" color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 1 border.width: 1
@ -463,11 +590,15 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: 4 spacing: 4
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
model: wifiLogic.networks model: Object.values(wifiLogic.networks)
delegate: Item { delegate: Item {
id: networkEntry id: networkEntry
required property var modelData
property var signalIcon: wifiPanel.signalIcon
width: parent.width 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 { ColumnLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
@ -504,7 +635,8 @@ Item {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
Item { Item {
width: 22; height: 22 width: 22
height: 22
visible: wifiLogic.connectStatusSsid === modelData.ssid && wifiLogic.connectStatus !== "" visible: wifiLogic.connectStatusSsid === modelData.ssid && wifiLogic.connectStatus !== ""
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@ -554,28 +686,29 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
Item { Item {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: 22 Layout.preferredHeight: 22
Layout.preferredWidth: 22 Layout.preferredWidth: 22
Spinner { Spinner {
visible: wifiLogic.connectingSsid === modelData.ssid visible: wifiLogic.connectingSsid === modelData.ssid
running: wifiLogic.connectingSsid === modelData.ssid running: wifiLogic.connectingSsid === modelData.ssid
color: Theme.accentPrimary color: Theme.accentPrimary
anchors.centerIn: parent anchors.centerIn: parent
size: 22 size: 22
} }
} }
} }
MouseArea { MouseArea {
id: networkMouseArea id: networkMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onClicked: { onClicked: {
if (modelData.connected) { // Toggle the action panel for this network
wifiLogic.disconnectNetwork(modelData.ssid); if (wifiLogic.actionPanelSsid === modelData.ssid) {
wifiLogic.actionPanelSsid = ""; // Close if already open
} else { } else {
wifiLogic.connectNetwork(modelData.ssid, modelData.security); wifiLogic.actionPanelSsid = modelData.ssid; // Open for this network
} }
} }
} }
@ -586,8 +719,9 @@ Item {
Layout.preferredHeight: 60 Layout.preferredHeight: 60
radius: 8 radius: 8
color: "transparent" color: "transparent"
anchors.leftMargin: 32 Layout.alignment: Qt.AlignLeft
anchors.rightMargin: 32 Layout.leftMargin: 32
Layout.rightMargin: 32
z: 2 z: 2
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@ -627,14 +761,18 @@ Item {
} }
} }
Rectangle { Rectangle {
width: 80 Layout.preferredWidth: 80
height: 36 Layout.preferredHeight: 36
radius: 18 radius: 18
color: Theme.accentPrimary color: Theme.accentPrimary
border.color: Theme.accentPrimary border.color: Theme.accentPrimary
border.width: 0 border.width: 0
opacity: 1.0 opacity: 1.0
Behavior on color { ColorAnimation { duration: 100 } } Behavior on color {
ColorAnimation {
duration: 100
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: wifiLogic.submitPassword() onClicked: wifiLogic.submitPassword()
@ -653,6 +791,114 @@ 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
}
}
}
}
} }
} }
} }