Improve network panel Wi-Fi grouping

This commit is contained in:
David Heinemeier Hansson
2026-05-22 09:46:41 +02:00
parent d0795793a3
commit 23b6cc29c9
+121 -57
View File
@@ -73,6 +73,7 @@ Panel {
// Index into `wifiNetworks` for keyboard navigation. -1 = no selection.
property int selectedIndex: -1
property bool wifiActionFocused: false
property bool cursorActive: false
// Keyboard focus zone for the panel. j/k crosses row boundaries:
@@ -130,6 +131,7 @@ Panel {
if (opened) {
refresh(true)
selectedIndex = wifiNetworks.length > 0 ? 0 : -1
wifiActionFocused = false
focusSection = wifiNetworks.length > 0 ? "wifi" : "dns"
var idx = dnsProviders.indexOf(dnsProvider)
dnsIndex = idx >= 0 ? idx : 0
@@ -161,6 +163,7 @@ Panel {
onWifiNetworksChanged: {
if (wifiNetworks.length === 0) {
selectedIndex = -1
wifiActionFocused = false
if (focusSection === "wifi") focusSection = "dns"
} else if (passwordSsid !== "") {
var passwordIndex = wifiIndexForSsid(passwordSsid)
@@ -173,6 +176,10 @@ Panel {
} else if (selectedIndex < 0 && opened) {
selectedIndex = 0
}
if (selectedIndex < 0 || selectedIndex >= wifiNetworks.length || !canForgetNetwork(wifiNetworks[selectedIndex])) {
wifiActionFocused = false
}
}
onWifiDeviceChanged: {
@@ -186,6 +193,21 @@ Panel {
if (wifiNetworks.length === 0) { selectedIndex = -1; return }
if (selectedIndex < 0) selectedIndex = delta > 0 ? 0 : wifiNetworks.length - 1
else selectedIndex = Math.max(0, Math.min(wifiNetworks.length - 1, selectedIndex + delta))
wifiActionFocused = false
}
function canForgetNetwork(net) {
return !!(net && net.known && isProtected(net.security) && !net.connected)
}
function selectWifiActionByDelta(delta) {
if (selectedIndex < 0 || selectedIndex >= wifiNetworks.length) return
if (!canForgetNetwork(wifiNetworks[selectedIndex])) {
wifiActionFocused = false
return
}
if (delta > 0) wifiActionFocused = true
else if (delta < 0) wifiActionFocused = false
}
// Enter/Space on the highlighted row. Mirrors row-click semantics:
@@ -195,20 +217,12 @@ Panel {
if (busy || selectedIndex < 0 || selectedIndex >= wifiNetworks.length) return
var net = wifiNetworks[selectedIndex]
if (!net) return
if (wifiActionFocused && canForgetNetwork(net)) { forget(net); return }
if (net.connected) { disconnect(net.network); return }
if (isProtected(net.security) && !net.known) { openPasswordPrompt(net.ssid); return }
connectKnown(net.ssid)
}
// 'x' on the highlighted row. Meaningful for saved/known networks
// (and the currently-connected row, if one is ever present); forget is
// hidden and a no-op otherwise.
function forgetSelected() {
if (busy || selectedIndex < 0 || selectedIndex >= wifiNetworks.length) return
var net = wifiNetworks[selectedIndex]
if (net && (net.connected || net.known)) forget(net)
}
// Bar pill state. Polled locally so this panel is self-contained;
// populated by networkProc + networkTimer below.
property string kind: "disconnected"
@@ -353,7 +367,6 @@ Panel {
checkActionCompletion(network)
var isConnected = network.connected
var ssid = network.name || ""
if (isConnected && ssid !== root.actionSsid) continue // Skip the connected network so it doesn't appear in the list, unless we are currently trying to connect to it
nets.push({
network: network,
connected: isConnected,
@@ -364,6 +377,8 @@ Panel {
})
}
nets.sort(function(a, b) {
if (a.connected !== b.connected) return a.connected ? -1 : 1
if (a.known !== b.known) return a.known ? -1 : 1
return b.signal - a.signal
})
wifiNetworks = nets
@@ -371,6 +386,17 @@ Panel {
scanning = false
}
function wifiSectionTitle(index) {
if (index < 0 || index >= wifiNetworks.length) return ""
var net = wifiNetworks[index]
if (!net) return ""
if (net.known && index === 0) return "KNOWN NETWORKS"
if (!net.known && (index === 0 || (wifiNetworks[index - 1] && wifiNetworks[index - 1].known))) return "OTHER NETWORKS"
return ""
}
function wifiIconFor(strength) {
var icons = ["󰤯", "󰤟", "󰤢", "󰤥", "󰤨"]
var index = Math.max(0, Math.min(4, Math.ceil(strength / 20) - 1))
@@ -673,13 +699,17 @@ Panel {
} else { // wifi
// k from the top row escapes back up into the DNS row rather
// than wrapping around to the bottom of the list.
if (dy < 0 && root.selectedIndex <= 0) root.focusSection = "dns"
if (dy < 0 && root.selectedIndex <= 0) {
root.focusSection = "dns"
root.wifiActionFocused = false
}
else root.selectByDelta(dy)
}
}
if (dx !== 0) {
if (root.focusSection === "header") root.selectHeaderByDelta(dx)
else if (root.focusSection === "dns") root.selectDnsByDelta(dx)
else if (root.focusSection === "wifi") root.selectWifiActionByDelta(dx)
}
}
onActivateRequested: {
@@ -690,9 +720,6 @@ Panel {
}
}
onCloseRequested: root.close()
onDeleteRequested: {
if (root.cursorActive && root.focusSection === "wifi") root.forgetSelected()
}
onTextKey: function(t) {
if (t === "r" || t === "R") root.refresh()
}
@@ -961,8 +988,8 @@ Panel {
}
PanelSectionHeader {
visible: root.wifiStationAvailable
text: root.scanning ? "SCANNING WI-FI…" : "WI-FI NETWORKS"
visible: root.wifiStationAvailable && root.scanning
text: "SCANNING WI-FI…"
foreground: root.bar.foreground
fontFamily: root.bar.fontFamily
}
@@ -994,13 +1021,29 @@ Panel {
delegate: Item {
required property var modelData
required property int index
readonly property string sectionTitle: root.wifiSectionTitle(index)
width: ListView.view.width
height: row.implicitHeight
NetworkRow {
id: row
height: delegateColumn.implicitHeight
Column {
id: delegateColumn
width: parent.width
net: parent.modelData
index: parent.index
spacing: Style.space(4)
PanelSectionHeader {
visible: sectionTitle !== ""
text: sectionTitle
foreground: root.bar.foreground
fontFamily: root.bar.fontFamily
height: visible ? implicitHeight : 0
}
NetworkRow {
id: row
width: parent.width
net: modelData
index: parent.parent.index
}
}
}
}
@@ -1041,7 +1084,7 @@ Panel {
// A single Wi-Fi network entry. Collapses to a one-line pill normally;
// expands inline to a passphrase prompt when the user picks a protected
// network we don't have credentials for. Clicking a connected row
// disconnects; the X button on any saved/connected row forgets it.
// disconnects.
component NetworkRow: CursorSurface {
id: row
required property var net
@@ -1050,10 +1093,12 @@ Panel {
readonly property bool isConnected: net && net.connected
readonly property bool isKnown: !!(net && net.known)
readonly property bool isProtected: net ? root.isProtected(net.security) : false
readonly property bool canForget: isConnected || isKnown
readonly property bool canForgetFromLock: isKnown && isProtected && !isConnected
readonly property bool isSelected: root.focusSection === "wifi" && root.selectedIndex === index
readonly property bool forgetFocused: isSelected && root.wifiActionFocused && canForgetFromLock
readonly property bool forgetVisible: canForgetFromLock && (forgetFocused || rightMouse.containsMouse)
hasCursor: root.cursorActive && isSelected
hasCursor: root.cursorActive && isSelected && !root.wifiActionFocused
current: isConnected
foreground: root.bar.foreground
fill: root.hoverFill
@@ -1115,7 +1160,7 @@ Panel {
// Move the cursor here when the mouse enters; mouse leaving doesn't
// clear it (so the cursor stays where the mouse last was and
// subsequent j/k pick up from this row).
onContainsMouseChanged: if (containsMouse) { root.cursorActive = true; root.focusSection = "wifi"; root.selectedIndex = row.index }
onContainsMouseChanged: if (containsMouse) { root.cursorActive = true; root.focusSection = "wifi"; root.selectedIndex = row.index; root.wifiActionFocused = false }
onClicked: {
if (!row.net) return
@@ -1124,6 +1169,7 @@ Panel {
root.cursorActive = true
root.focusSection = "wifi"
root.selectedIndex = row.index
root.wifiActionFocused = false
if (row.isConnected) {
root.disconnect(row.net.network)
return
@@ -1143,7 +1189,7 @@ Panel {
anchors.top: parent.top
anchors.leftMargin: Style.space(10)
anchors.rightMargin: Style.space(10)
implicitHeight: Math.max(networkIcon.implicitHeight, networkInfo.implicitHeight, forgetBtn.implicitHeight) + Style.spacing.rowPaddingX
implicitHeight: Math.max(networkIcon.implicitHeight, networkInfo.implicitHeight, rightAction.implicitHeight) + Style.spacing.rowPaddingX
Text {
id: networkIcon
@@ -1155,34 +1201,53 @@ Panel {
anchors.verticalCenter: parent.verticalCenter
}
PanelActionButton {
id: forgetBtn
anchors.right: lockIndicator.visible ? lockIndicator.left : parent.right
anchors.rightMargin: lockIndicator.visible ? Style.space(4) : 0
anchors.verticalCenter: parent.verticalCenter
visible: row.canForget
enabled: !root.busy
iconText: "󰅙"
tooltipText: "Forget network"
foreground: root.bar.foreground
hoverColor: root.bar.urgent
fontFamily: root.bar.fontFamily
onClicked: if (row.net) root.forget(row.net)
}
// Shows a lock glyph for protected disconnected networks at the far
// right, with the forget X to its left when the network is saved.
Text {
id: lockIndicator
visible: row.isProtected && !row.isConnected
// Shows a lock glyph for protected networks. Known disconnected
// networks reveal the forget action when hovering that right edge.
Item {
id: rightAction
visible: row.isProtected
width: Style.space(22)
implicitHeight: lockIndicator.implicitHeight
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
text: "󰌾"
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.subtitle
Text {
id: lockIndicator
width: parent.width
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
text: row.forgetVisible ? "󰅙" : "󰌾"
color: row.forgetVisible ? root.bar.urgent : Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.subtitle
}
Rectangle {
anchors.fill: parent
visible: row.forgetFocused
color: Style.hoverFillFor(root.bar.urgent, root.bar.urgent)
border.color: Style.hoverBorderFor(root.bar.urgent, root.bar.urgent)
border.width: Style.hoverBorderWidth
radius: Style.cornerRadius
z: -1
}
MouseArea {
id: rightMouse
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
enabled: row.canForgetFromLock && !root.busy
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onContainsMouseChanged: if (containsMouse) { root.cursorActive = true; root.focusSection = "wifi"; root.selectedIndex = row.index; root.wifiActionFocused = true }
onClicked: if (row.net) root.forget(row.net)
}
PanelToolTip {
visible: rightMouse.containsMouse || row.forgetFocused
text: "Forget network"
fontFamily: root.bar.fontFamily
}
}
Column {
@@ -1190,10 +1255,9 @@ Panel {
spacing: Style.space(1)
anchors.left: networkIcon.right
anchors.leftMargin: Style.space(10)
anchors.right: forgetBtn.visible ? forgetBtn.left
: lockIndicator.visible ? lockIndicator.left
anchors.right: rightAction.visible ? rightAction.left
: parent.right
anchors.rightMargin: (forgetBtn.visible || lockIndicator.visible) ? Style.space(8) : 0
anchors.rightMargin: rightAction.visible ? Style.space(8) : 0
anchors.verticalCenter: parent.verticalCenter
Text {
@@ -1257,7 +1321,7 @@ Panel {
anchors.rightMargin: Style.space(6)
password: true
placeholderText: "Passphrase"
font.family: root.bar.fontFamily
font.family: Style.font.family
font.pixelSize: Style.font.body
foreground: root.bar.foreground
horizontalPadding: Style.spacing.controlGap
@@ -1298,8 +1362,8 @@ Panel {
}
}
// 22×22 right-anchored to line up with forgetBtn and lockIndicator
// above. Esc closes the prompt (handled by pwField.Keys.onEscapePressed)
// 22×22 right-anchored to line up with lockIndicator above. Esc closes
// the prompt (handled by pwField.Keys.onEscapePressed)
// so there's no separate cancel button.
PanelActionButton {
id: connectPwBtn