nosignal-shell/modules/nsbar/panels/NsNetwork.qml
28allday e42538896f fix(nosignal): hardware-test round 4 — Wi-Fi join, multi-monitor popouts, sudo focus
From the 2026-06-20 Acer muxless-Optimus hardware test:

- F-wifi (NsNetwork.qml): the Wi-Fi panel was display-only — no onClicked,
  no password field — so a secured network could not be joined from the GUI.
  Wire the existing Network service: click to connect; open/saved networks
  connect directly; secured-without-profile expands an inline password field
  (StyledTextField, echoMode Password). Also strip a stray NUL byte that the
  on-box edit left at the `?? " "` sentinel. HW-verified on the test laptop.
- F-multimon (NsShell.qml + NsBar.qml + NsOverlay.qml): bar popouts mirrored
  on every monitor. Add a `screen` identity to NsShell; pills pass their
  output name; each NsOverlay only renders when it owns the open panel ("" =
  unscoped, for the global power modal). HW-verified dual-monitor.
- F-sudo (SudoToggleRow.qml): the passwordless-sudo prompt used the shared
  TUI.float class with no focus rule, so keystrokes missed it and three blank
  tries tripped pam_faillock with no feedback. Use a dedicated `nosignal-sudo`
  class (builder ships float/center/pin/stayfocused rules) and call the new
  `nosignal-sudo-toggle enable-tui` wrapper, which keeps the terminal open on
  failure so the reason is visible.

Add .gitattributes (*.qml/*.conf/*.json text) for clean diffs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:49:18 +01:00

237 lines
8 KiB
QML

pragma ComponentBehavior: Bound
// NoSignal Network popout (off the Wi-Fi pill). Title + switch; list of networks
// (signal glyph accent if connected, SSID, Connected label, lock if secured);
// connected row = accent.soft. Footer "Network settings". Wired to Network/Nmcli.
//
// FIX 2026-06-20 (hardware test): the shipped panel was display-only — the row
// MouseArea had no onClicked and there was no password field, so a secured Wi-Fi
// network could not be joined from the GUI (user had to drop to nmcli/nmtui).
// This wires the existing Network service: click a row to connect; open or
// already-saved networks connect directly; a secured network with no saved
// profile expands an inline password field (StyledTextField, echoMode Password).
// NsOverlay grants OnDemand keyboard focus while visible, so the field is typable.
// NOTE: authored on the test machine but NOT yet built/cert'd — review in builder.
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.utils
import qs.components
import qs.components.controls
NsPanel {
id: root
implicitWidth: 340
anchorMode: "right"
// ssid of the secured row currently showing its inline password field ("" = none)
property string expandedSsid: ""
// Decide what a click on a network row does.
function activate(m): void {
if (!m)
return;
if (m.active) {
// Already connected -> tapping disconnects.
Network.disconnectFromNetwork();
root.expandedSsid = "";
return;
}
if (!m.isSecure || Network.hasSavedProfile(m.ssid)) {
// Open network, or we already hold the PSK -> connect straight away.
Network.connectToNetworkWithPasswordCheck(m.ssid, m.isSecure, () => {}, m.bssid);
root.expandedSsid = "";
} else {
// Secured + no saved profile -> reveal the inline password field.
root.expandedSsid = (root.expandedSsid === m.ssid) ? "" : m.ssid;
}
}
RowLayout {
Layout.fillWidth: true
StyledText {
text: "Wi-Fi"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.panelTitle
font.weight: Font.DemiBold
}
Item {
Layout.fillWidth: true
}
NsSwitch {
on: Network.wifiEnabled
onToggled: Network.toggleWifi()
}
}
// network list
ColumnLayout {
Layout.fillWidth: true
spacing: 2
visible: Network.wifiEnabled
Repeater {
model: Network.networks
delegate: ColumnLayout {
id: rowWrap
required property var modelData
readonly property bool connected: modelData?.active ?? false
readonly property bool expanded: root.expandedSsid === (modelData?.ssid ?? " ")
Layout.fillWidth: true
spacing: 2
function doConnect(): void {
const m = rowWrap.modelData;
if (!m || pwField.text.length === 0)
return;
Network.connectToNetwork(m.ssid, pwField.text, m.bssid, () => {});
pwField.text = "";
root.expandedSsid = "";
}
Rectangle {
id: row
Layout.fillWidth: true
implicitHeight: 40
radius: Theme.radius.button
color: rowWrap.connected ? Theme.accentSoft : ma.containsMouse ? Theme.hover : "transparent"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 10
NsIcon {
icon: Icons.getNetworkIcon(rowWrap.modelData?.strength ?? 0)
color: rowWrap.connected ? Theme.accent : Theme.text
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
text: rowWrap.modelData?.ssid ?? "?"
color: Theme.text
elide: Text.ElideRight
Layout.fillWidth: true
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
StyledText {
visible: rowWrap.connected
text: "Connected"
color: Theme.accent
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
NsIcon {
visible: rowWrap.modelData?.isSecure ?? false
icon: "lock"
color: Theme.textFaint
}
}
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activate(rowWrap.modelData)
}
}
// inline password entry — only for a secured network with no saved profile
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.bottomMargin: 4
spacing: 8
visible: rowWrap.expanded
onVisibleChanged: {
if (visible)
pwField.forceActiveFocus();
}
StyledTextField {
id: pwField
Layout.fillWidth: true
echoMode: TextField.Password
placeholderText: "Password"
onAccepted: rowWrap.doConnect()
}
Rectangle {
implicitWidth: 72
implicitHeight: 28
radius: Theme.radius.button
color: cma.containsMouse ? Theme.accent : Theme.accentSoft
StyledText {
anchors.centerIn: parent
text: "Connect"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
MouseArea {
id: cma
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: rowWrap.doConnect()
}
}
}
}
}
StyledText {
visible: Network.networks.length === 0
text: "No networks found"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}
StyledText {
visible: !Network.wifiEnabled
text: "Wi-Fi is off"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
// footer
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 4
implicitHeight: 1
color: Theme.barBorder
}
StyledText {
text: "Network settings"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
}