Keyboard-driven wifi panel
The wifi panel was a bar-widget xdg-popup which Hyprland doesn't grant keyboard focus to until the user clicks or hovers it — keys typed after a SUPER+CTRL+W summon went to whatever previously had focus. New shared Common.KeyboardPanel scaffolding: layer-shell PanelWindow with WlrKeyboardFocus.Exclusive (granted at map time, the protocol-level equivalent of focus-on-launch for xdg-toplevels), full-screen anchored with a Region mask that subtracts the bar strip so bar widgets stay clickable, MouseArea for outside-click dismissal, TransformWatcher for reactive anchor-position tracking. API is a subset of Common.PopupCard (no centerOnBar / triggerMode / containsMouse yet — adding when the other bar popups migrate). networkPanel.qml uses it. Adds full keyboard navigation: j/k or Up/Down — move selection Return/Space — connect (or disconnect if already connected) x — forget the highlighted network r — refresh scan Esc — close ListView (with positionViewAtIndex on currentIndex) replaces the Repeater+Column inside Flickable so the selected row stays visible as j/k walks past the cap. A 1.5s Timer re-polls detailsProc while open so the connection details (IP, gateway, signal) populate when routing actually comes up after a connect rather than waiting for re-summon. Rows: lock glyph on the right for protected-and-not-connected, X (forget) for connected — aligned to the same 22-wide column. Connect glyph in the passphrase prompt switched to a check, no separate cancel (Esc handles it). SUPER+CTRL+W in utilities.lua now toggles this panel via omarchy-shell-ipc instead of launching Impala. Right-click on the bar icon still launches Impala as a fallback for hidden SSIDs / enterprise auth / iwd edge cases.
This commit is contained in:
@@ -55,7 +55,7 @@ hl.bind("SUPER + CTRL + ALT + W", hl.dsp.exec_cmd("omarchy-notification-weather"
|
||||
|
||||
hl.bind("SUPER + CTRL + A", hl.dsp.exec_cmd("omarchy-launch-audio"), { description = "Audio controls" })
|
||||
hl.bind("SUPER + CTRL + B", hl.dsp.exec_cmd("omarchy-launch-bluetooth"), { description = "Bluetooth controls" })
|
||||
hl.bind("SUPER + CTRL + W", hl.dsp.exec_cmd("omarchy-launch-wifi"), { description = "Wifi controls" })
|
||||
hl.bind("SUPER + CTRL + W", hl.dsp.exec_cmd("omarchy-shell-ipc networkPanel toggle"), { description = "Wifi panel" })
|
||||
hl.bind("SUPER + CTRL + T", hl.dsp.exec_cmd("omarchy-launch-tui btop"), { description = "Activity" })
|
||||
|
||||
hl.bind("SUPER + CTRL + X", hl.dsp.exec_cmd("voxtype record toggle"), { description = "Toggle dictation" })
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
|
||||
// Layer-shell popup attached to a bar widget icon, designed for
|
||||
// click-driven AND keyboard-driven panels (e.g. SUPER+CTRL+W summon).
|
||||
//
|
||||
// Built on PanelWindow with WlrKeyboardFocus.Exclusive rather than
|
||||
// PopupWindow (xdg-popup). Layer-shell surfaces declared Exclusive get
|
||||
// keyboard focus from Hyprland *at map time*, which is the protocol-level
|
||||
// equivalent of focus-on-launch for xdg-toplevels. xdg-popups don't get
|
||||
// that — they only receive keys after a click/hover routes focus through
|
||||
// their parent surface — so keyboard-summoned popups fell flat without it.
|
||||
//
|
||||
// API is a subset of Common.PopupCard: anchorItem, owner, bar, open,
|
||||
// padding, margin, contentWidth/Height, default contentItem. Missing on
|
||||
// purpose (for now): centerOnBar, triggerMode ("hover"), containsMouse.
|
||||
// Hover-mode popups (system-stats, weather-flyout) and centered popups
|
||||
// (calendar week-view) need extra plumbing before migrating; converting
|
||||
// them is a follow-up.
|
||||
//
|
||||
// Positioning: full-screen layer-shell with the card placed inside at
|
||||
// `cardOrigin`. We use the bar window's height/width for the perpendicular
|
||||
// axis (away-from-bar) because mapToItem on the anchor returns
|
||||
// bar-content-relative coords with internal layout offsets baked in
|
||||
// (e.g. ~13px from the bar's vertical centering of its widget row). The
|
||||
// parallel axis (along-the-bar) uses the anchor's content x/y since the
|
||||
// bar spans full screen on that axis.
|
||||
//
|
||||
// Outside-click dismissal: an overlay MouseArea catches clicks, with the
|
||||
// QsWindow.mask subtracting the bar strip so clicks on the bar still
|
||||
// reach the bar widgets (activePopout coordinator hands off to another
|
||||
// popup if the user clicks a different bar icon).
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property Item anchorItem
|
||||
required property QtObject bar
|
||||
property var owner: null
|
||||
property int margin: 10
|
||||
property int padding: 14
|
||||
property int contentWidth: 280
|
||||
property int contentHeight: 200
|
||||
property bool open: false
|
||||
property int gap: 10 // distance between bar edge and panel
|
||||
|
||||
default property alias contentItem: contentHolder.children
|
||||
|
||||
readonly property var coordinatorKey: owner || root
|
||||
readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null
|
||||
readonly property string barPos: bar ? bar.position : "top"
|
||||
|
||||
function closePopout() {
|
||||
if (owner && "closePopout" in owner) owner.closePopout()
|
||||
else root.open = false
|
||||
}
|
||||
|
||||
// --- screen + lifetime ---------------------------------------------------
|
||||
|
||||
screen: anchorWindow ? anchorWindow.screen : null
|
||||
visible: open || card.opacity > 0
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WlrLayershell.namespace: "omarchy-keyboard-panel"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
// Keyboard focus follows `open` (NOT `visible`). The window remains
|
||||
// mapped during the fade-out so the opacity animation has something to
|
||||
// animate, but keyboard/click ownership must release the moment the
|
||||
// logical close fires — otherwise the user is locked out for 140ms.
|
||||
WlrLayershell.keyboardFocus: open ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
|
||||
// Full-screen layer-shell. The visible card is positioned inside via
|
||||
// `cardOrigin`. The `mask` below makes the bar area click-through (so
|
||||
// the user can click another bar icon while the panel is open and the
|
||||
// activePopout coordinator swaps to that popup); everywhere else, the
|
||||
// overlay catches the click and dismisses via the MouseArea below.
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
// Clickable region = whole screen MINUS the bar's strip. Clicks on the
|
||||
// bar pass through to the bar layer; clicks anywhere else are caught
|
||||
// by us and either land on the card (no-op) or trigger dismissal.
|
||||
readonly property real _barStripSize: bar ? bar.barSize : 0
|
||||
mask: Region {
|
||||
width: root.screenW
|
||||
height: root.screenH
|
||||
Region {
|
||||
x: root.barPos === "right" ? root.screenW - root._barStripSize : 0
|
||||
y: root.barPos === "bottom" ? root.screenH - root._barStripSize : 0
|
||||
width: (root.barPos === "top" || root.barPos === "bottom") ? root.screenW : root._barStripSize
|
||||
height: (root.barPos === "top" || root.barPos === "bottom") ? root._barStripSize : root.screenH
|
||||
intersection: Intersection.Subtract
|
||||
}
|
||||
}
|
||||
|
||||
// Track every layout change between the bar's contentItem and the
|
||||
// anchor item. `transform` updates whenever any item in that chain
|
||||
// moves/resizes, which is what makes the position binding below
|
||||
// actually reactive — mapToItem on its own is a one-shot.
|
||||
TransformWatcher {
|
||||
id: anchorWatcher
|
||||
a: anchorWindow ? anchorWindow.contentItem : null
|
||||
b: anchorItem
|
||||
}
|
||||
|
||||
// Anchor item's position within the bar's content surface. For a
|
||||
// full-width top bar, the content x maps directly to screen x; the y
|
||||
// returned here has the bar's internal padding baked in (e.g. ~13px
|
||||
// from vertical centering of the widget row), which is why `cardOrigin`
|
||||
// below uses `barH` for the perpendicular axis instead of this y.
|
||||
readonly property point anchorScreenPos: {
|
||||
anchorWatcher.transform // reactive dependency
|
||||
if (!anchorItem || !anchorWindow) return Qt.point(0, 0)
|
||||
return anchorItem.mapToItem(anchorWindow.contentItem, 0, 0)
|
||||
}
|
||||
readonly property real anchorW: anchorItem ? anchorItem.width : 0
|
||||
readonly property real anchorH: anchorItem ? anchorItem.height : 0
|
||||
readonly property real screenW: screen ? screen.width : 0
|
||||
readonly property real screenH: screen ? screen.height : 0
|
||||
|
||||
// Desired top-left of the card in screen coordinates. For the
|
||||
// perpendicular axis (away-from-bar) we anchor to the bar window's edge
|
||||
// directly — not the anchor item's y/x — because mapToItem(barContent)
|
||||
// returns coordinates in the bar's content space, which can be offset
|
||||
// from the bar surface's screen-anchored corner by internal layout
|
||||
// (centering wrappers, padding). The bar's surface IS aligned to its
|
||||
// anchored screen edge, so using `barW`/`barH` gives the right edge
|
||||
// regardless of how the bar's internal widgets are positioned. For the
|
||||
// parallel axis (along the bar) the anchor item's reported position is
|
||||
// still consistent with the bar content origin, so it's accurate for
|
||||
// centering the card under the icon.
|
||||
readonly property real barW: anchorWindow ? anchorWindow.width : screenW
|
||||
readonly property real barH: anchorWindow ? anchorWindow.height : 0
|
||||
readonly property point cardOrigin: {
|
||||
if (!anchorItem || !bar) return Qt.point(margin, margin)
|
||||
var x = 0, y = 0
|
||||
if (barPos === "bottom") {
|
||||
x = anchorScreenPos.x + anchorW / 2 - contentWidth / 2
|
||||
y = screenH - barH - contentHeight - gap
|
||||
} else if (barPos === "left") {
|
||||
x = barW + gap
|
||||
y = anchorScreenPos.y + anchorH / 2 - contentHeight / 2
|
||||
} else if (barPos === "right") {
|
||||
x = screenW - barW - contentWidth - gap
|
||||
y = anchorScreenPos.y + anchorH / 2 - contentHeight / 2
|
||||
} else { // "top" (default)
|
||||
x = anchorScreenPos.x + anchorW / 2 - contentWidth / 2
|
||||
y = barH + gap
|
||||
}
|
||||
x = Math.max(margin, Math.min(x, screenW - contentWidth - margin))
|
||||
y = Math.max(margin, Math.min(y, screenH - contentHeight - margin))
|
||||
return Qt.point(Math.round(x), Math.round(y))
|
||||
}
|
||||
|
||||
|
||||
// --- popout coordination (same-bar single-popout model) -----------------
|
||||
|
||||
// Coordinate on `open`, not `visible`. `visible` lags into the fade-out
|
||||
// animation, which made ownership transfer to a sibling popup race.
|
||||
onOpenChanged: {
|
||||
if (!bar) return
|
||||
if (open) bar.requestPopout(coordinatorKey)
|
||||
else if (bar.activePopout === coordinatorKey) bar.releasePopout(coordinatorKey)
|
||||
}
|
||||
|
||||
// --- outside-click dismissal --------------------------------------------
|
||||
|
||||
// Catches clicks anywhere in the clickable region (i.e. everywhere on
|
||||
// screen except the bar strip, which is masked out). The card has its
|
||||
// own MouseArea below so clicks on it don't bubble up here. Disabled
|
||||
// during the fade-out so the dying overlay doesn't swallow clicks that
|
||||
// were meant for the apps behind it.
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.open
|
||||
onClicked: root.closePopout()
|
||||
}
|
||||
|
||||
// --- card ----------------------------------------------------------------
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
x: root.cardOrigin.x
|
||||
y: root.cardOrigin.y
|
||||
width: root.contentWidth
|
||||
height: root.contentHeight
|
||||
color: Color.popups.background
|
||||
border.color: Color.popups.border
|
||||
border.width: 2
|
||||
radius: 0
|
||||
opacity: root.open ? 1.0 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
// Swallow clicks on the card so they don't bubble to the dismissal
|
||||
// MouseArea behind us.
|
||||
MouseArea { anchors.fill: parent }
|
||||
|
||||
Item {
|
||||
id: contentHolder
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.padding
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,11 @@ Item {
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
function closePopout() { popupOpen = false }
|
||||
// Centralized close so callers can't forget to drop the passphrase prompt.
|
||||
function closePopout() {
|
||||
popupOpen = false
|
||||
passwordSsid = ""
|
||||
}
|
||||
|
||||
// Live connection details from `ip` / /sys / iw.
|
||||
property var info: ({}) // { iface, type, ip, prefix, gateway, speed, duplex, ssid, signal, freq, bitrate }
|
||||
@@ -22,6 +26,95 @@ Item {
|
||||
property string dnsProvider: ""
|
||||
property string pendingDnsProvider: ""
|
||||
|
||||
// Per-row in-flight state. `actionSsid` flips on for the row whose action
|
||||
// is currently running so it can render "Connecting…" / "Disconnecting…" /
|
||||
// "Forgetting…". `passwordSsid` is the row currently expanded into
|
||||
// password-entry mode; we keep it open across refresh cycles so a slow scan
|
||||
// doesn't collapse the input the user is typing into. Rows must gate
|
||||
// comparisons on the matching `*Kind`/`*Reason` being non-empty so a
|
||||
// hidden-SSID row (ssid == "") doesn't collide with the "" defaults.
|
||||
property string actionSsid: ""
|
||||
property string actionKind: "" // "connect" | "disconnect" | "forget"
|
||||
property string failureSsid: ""
|
||||
property string failureReason: ""
|
||||
property string passwordSsid: ""
|
||||
|
||||
// True while any wifi action or known-network probe is mid-flight. Rows
|
||||
// disable themselves on this so clicks on the other rows don't silently
|
||||
// no-op against runAction's serialized guard.
|
||||
readonly property bool busy: actionProc.running || knownCheck.running
|
||||
|
||||
// Index into `wifiNetworks` for keyboard navigation. -1 = no selection.
|
||||
property int selectedIndex: -1
|
||||
|
||||
// The panel below is its own layer-shell with Exclusive keyboard focus,
|
||||
// so Hyprland grants focus when the surface is mapped (popupOpen flips
|
||||
// to true). That's what makes the SUPER+CTRL+W keybind actually work
|
||||
// — OnDemand only grants focus on click/hover.
|
||||
onPopupOpenChanged: {
|
||||
if (popupOpen) {
|
||||
refresh()
|
||||
selectedIndex = wifiNetworks.length > 0 ? 0 : -1
|
||||
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
}
|
||||
|
||||
// When the passphrase prompt closes (Esc / Cancel / success) restore
|
||||
// focus to the keyCatcher so j/k/Enter resume working without a click.
|
||||
onPasswordSsidChanged: {
|
||||
if (passwordSsid === "" && popupOpen) {
|
||||
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
}
|
||||
|
||||
// Keep selectedIndex valid as scans refresh the network list.
|
||||
onWifiNetworksChanged: {
|
||||
if (wifiNetworks.length === 0) selectedIndex = -1
|
||||
else if (selectedIndex >= wifiNetworks.length) selectedIndex = wifiNetworks.length - 1
|
||||
else if (selectedIndex < 0 && popupOpen) selectedIndex = 0
|
||||
}
|
||||
|
||||
function selectByDelta(delta) {
|
||||
if (wifiNetworks.length === 0) { selectedIndex = -1; return }
|
||||
if (selectedIndex < 0) selectedIndex = delta > 0 ? 0 : wifiNetworks.length - 1
|
||||
else selectedIndex = (selectedIndex + delta + wifiNetworks.length) % wifiNetworks.length
|
||||
}
|
||||
|
||||
// Enter/Space on the highlighted row. Mirrors row-click semantics:
|
||||
// connected → disconnect, protected-unknown → password prompt (via
|
||||
// known-network probe), open/known → connect.
|
||||
function activateSelected() {
|
||||
if (busy || selectedIndex < 0 || selectedIndex >= wifiNetworks.length) return
|
||||
var net = wifiNetworks[selectedIndex]
|
||||
if (!net) return
|
||||
if (net.connected) { disconnect(net.ssid); return }
|
||||
if (isProtected(net.security)) {
|
||||
var quotedSsid = bar.shellQuote(net.ssid)
|
||||
knownCheck.targetSsid = net.ssid
|
||||
knownCheck.command = ["bash", "-c", `
|
||||
iwctl known-networks list 2>/dev/null \\
|
||||
| sed -e 's/\\x1b\\[[0-9;]*m//g' \\
|
||||
| awk -v s=${quotedSsid} 'NR > 3 {
|
||||
line = $0
|
||||
sub(/^[[:space:]]+/, "", line)
|
||||
sub(/[[:space:]]+$/, "", line)
|
||||
n = split(line, p, /[[:space:]]{2,}/)
|
||||
if (n >= 1 && p[1] == s) { print "yes"; exit }
|
||||
}'`]
|
||||
knownCheck.running = true
|
||||
return
|
||||
}
|
||||
connectKnown(net.ssid)
|
||||
}
|
||||
|
||||
// 'x' on the highlighted row. Only meaningful on the currently-connected
|
||||
// network — forget is a no-op (and the X icon is hidden) otherwise.
|
||||
function forgetSelected() {
|
||||
if (busy || selectedIndex < 0 || selectedIndex >= wifiNetworks.length) return
|
||||
var net = wifiNetworks[selectedIndex]
|
||||
if (net && net.connected) forget(net.ssid)
|
||||
}
|
||||
|
||||
readonly property string kind: bar ? bar.networkKind : "disconnected"
|
||||
readonly property string label: bar ? bar.networkLabel : ""
|
||||
readonly property int signalStrength: bar ? bar.networkSignal : -1
|
||||
@@ -134,11 +227,104 @@ Item {
|
||||
root.popupOpen = false
|
||||
}
|
||||
|
||||
function isProtected(security) {
|
||||
var s = String(security || "").toLowerCase()
|
||||
return s !== "" && s !== "open"
|
||||
}
|
||||
|
||||
function runAction(kind, ssid, command) {
|
||||
if (actionProc.running) return
|
||||
actionSsid = ssid
|
||||
actionKind = kind
|
||||
failureSsid = ""
|
||||
failureReason = ""
|
||||
actionProc.command = ["bash", "-c", command]
|
||||
actionProc.running = true
|
||||
// Safety net: if onExited never fires (process death, signal handler
|
||||
// throws, etc.), clear the busy state so the row doesn't get stuck on
|
||||
// "Connecting…" / "Disconnecting…" forever.
|
||||
actionTimeout.restart()
|
||||
}
|
||||
|
||||
function connectKnown(ssid) {
|
||||
var quotedSsid = bar.shellQuote(ssid)
|
||||
runAction("connect", ssid, `
|
||||
station=$(iwctl station list 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g' | awk '/^[[:space:]]*wl/ { print $1; exit }')
|
||||
[[ -z $station ]] && { echo "no Wi-Fi station available" >&2; exit 1; }
|
||||
iwctl --dont-ask station "$station" connect ${quotedSsid}
|
||||
`)
|
||||
}
|
||||
|
||||
function connectWithPassphrase(ssid, passphrase) {
|
||||
var quotedSsid = bar.shellQuote(ssid)
|
||||
var quotedPass = bar.shellQuote(passphrase)
|
||||
runAction("connect", ssid, `
|
||||
station=$(iwctl station list 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g' | awk '/^[[:space:]]*wl/ { print $1; exit }')
|
||||
[[ -z $station ]] && { echo "no Wi-Fi station available" >&2; exit 1; }
|
||||
iwctl --passphrase ${quotedPass} station "$station" connect ${quotedSsid}
|
||||
`)
|
||||
}
|
||||
|
||||
function disconnect(ssid) {
|
||||
runAction("disconnect", ssid, `
|
||||
station=$(iwctl station list 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g' | awk '/^[[:space:]]*wl/ { print $1; exit }')
|
||||
[[ -z $station ]] && { echo "no Wi-Fi station available" >&2; exit 1; }
|
||||
iwctl station "$station" disconnect
|
||||
`)
|
||||
}
|
||||
|
||||
// iwd doesn't reliably tear down an active connection when you forget the
|
||||
// network underneath it, so if the station is currently on this SSID we
|
||||
// disconnect first and bail on failure rather than reporting a misleading
|
||||
// success. If the station isn't on this SSID (or there is no station),
|
||||
// skip straight to forget.
|
||||
function forget(ssid) {
|
||||
var quotedSsid = bar.shellQuote(ssid)
|
||||
runAction("forget", ssid, `
|
||||
station=$(iwctl station list 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g' | awk '/^[[:space:]]*wl/ { print $1; exit }')
|
||||
if [[ -n $station ]]; then
|
||||
current=$(iwctl station "$station" show 2>/dev/null \
|
||||
| sed -e 's/\\x1b\\[[0-9;]*m//g' \
|
||||
| awk '/Connected network/ {
|
||||
s = $0
|
||||
sub(/.*Connected network[[:space:]]+/, "", s)
|
||||
sub(/[[:space:]]+$/, "", s)
|
||||
print s
|
||||
exit
|
||||
}')
|
||||
if [[ $current == ${quotedSsid} ]]; then
|
||||
iwctl station "$station" disconnect || { echo "failed to disconnect before forget" >&2; exit 1; }
|
||||
fi
|
||||
fi
|
||||
iwctl known-networks ${quotedSsid} forget
|
||||
`)
|
||||
}
|
||||
|
||||
function launchImpala() {
|
||||
if (!bar) return
|
||||
var quotedPath = bar.shellQuote(bar.omarchyPath)
|
||||
var quotedBin = bar.shellQuote(bar.omarchyPath + "/bin/omarchy-launch-wifi")
|
||||
var quotedFullPath = bar.shellQuote(bar.omarchyPath + "/bin:" + (Quickshell.env("PATH") || ""))
|
||||
bar.run("OMARCHY_PATH=" + quotedPath + " PATH=" + quotedFullPath + " " + quotedBin)
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
// Lets a Hyprland keybind summon the panel without needing to click the
|
||||
// bar icon. Paired with the SUPER+CTRL+W binding in utilities.lua.
|
||||
IpcHandler {
|
||||
target: "networkPanel"
|
||||
function toggle(): void {
|
||||
if (root.popupOpen) root.closePopout()
|
||||
else root.popupOpen = true
|
||||
}
|
||||
function show(): void { if (!root.popupOpen) root.popupOpen = true }
|
||||
function hide(): void { root.closePopout() }
|
||||
}
|
||||
|
||||
// Pulls everything we want about the active route's interface in one shot.
|
||||
Process {
|
||||
id: detailsProc
|
||||
@@ -219,13 +405,83 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
}
|
||||
}
|
||||
|
||||
// Action runner for connect/disconnect/forget and DNS provider changes.
|
||||
// Streams stderr for wifi actions so we can surface "Operation failed" or
|
||||
// "Invalid passphrase" inline rather than dropping the user back to a
|
||||
// silent UI. setDns() and runAction() both gate on `actionProc.running`,
|
||||
// so the two flows can't overlap on this shared process.
|
||||
Process {
|
||||
id: actionProc
|
||||
stdout: StdioCollector { id: actionStdout; waitForEnd: true }
|
||||
stderr: StdioCollector { id: actionStderr; waitForEnd: true }
|
||||
onExited: function(exitCode) {
|
||||
// DNS change finished — pendingDnsProvider is the in-flight marker
|
||||
// for that flow (setDns doesn't touch actionKind), so handle it and
|
||||
// return before the wifi-action path.
|
||||
if (root.pendingDnsProvider !== "") {
|
||||
if (exitCode === 0) root.dnsProvider = root.pendingDnsProvider
|
||||
root.pendingDnsProvider = ""
|
||||
return
|
||||
}
|
||||
// Timeout already cleared our state and killed us — the exit signal
|
||||
// is stale, don't clobber whatever the user has done since.
|
||||
if (root.actionKind === "") return
|
||||
actionTimeout.stop()
|
||||
var ssid = root.actionSsid
|
||||
var kind = root.actionKind
|
||||
if (exitCode === 0) {
|
||||
if (kind === "connect") root.passwordSsid = ""
|
||||
root.failureSsid = ""
|
||||
root.failureReason = ""
|
||||
} else {
|
||||
root.failureSsid = ssid
|
||||
var reason = (actionStderr.text || actionStdout.text || "").trim()
|
||||
if (!reason) {
|
||||
if (kind === "connect") reason = "Failed to connect"
|
||||
else if (kind === "disconnect") reason = "Failed to disconnect"
|
||||
else reason = "Failed to forget"
|
||||
}
|
||||
// Squash multi-line iwctl errors into a single readable line.
|
||||
root.failureReason = reason.split("\n").pop()
|
||||
}
|
||||
root.actionSsid = ""
|
||||
root.actionKind = ""
|
||||
root.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// Poll detailsProc while the panel is open. `iwctl connect` returns
|
||||
// success the moment iwd accepts credentials — the IP/route isn't
|
||||
// actually assigned until a beat later, so a single post-action refresh
|
||||
// races against routing and the header stays blank. Polling fills the
|
||||
// details in as soon as the route comes up; cheap since the script is
|
||||
// small and only runs while the panel is visible.
|
||||
Timer {
|
||||
id: detailsPoll
|
||||
interval: 1500
|
||||
repeat: true
|
||||
running: root.popupOpen
|
||||
onTriggered: if (!detailsProc.running) detailsProc.running = true
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: actionTimeout
|
||||
interval: 15000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!root.actionKind) return
|
||||
var reason
|
||||
if (root.actionKind === "connect") reason = "Timed out connecting"
|
||||
else if (root.actionKind === "disconnect") reason = "Timed out disconnecting"
|
||||
else reason = "Timed out forgetting"
|
||||
// Clear state *before* killing the process so the eventual onExited
|
||||
// sees actionKind === "" and bails out as stale.
|
||||
root.failureSsid = root.actionSsid
|
||||
root.failureReason = reason
|
||||
root.actionSsid = ""
|
||||
root.actionKind = ""
|
||||
if (actionProc.running) actionProc.running = false // SIGTERM
|
||||
root.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,15 +495,20 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
tooltipText: bar ? bar.networkTooltip() : ""
|
||||
|
||||
onPressed: function(b) {
|
||||
if (b === Qt.RightButton) root.bar.run(root.bar.omarchyPath + "/bin/omarchy-launch-wifi")
|
||||
else {
|
||||
root.popupOpen = !root.popupOpen
|
||||
if (root.popupOpen) root.refresh()
|
||||
}
|
||||
if (b === Qt.RightButton) root.launchImpala()
|
||||
else if (root.popupOpen) root.closePopout()
|
||||
else { root.popupOpen = true; root.refresh() }
|
||||
}
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
// Keyboard-driven popup anchored to the bar widget icon. The shared
|
||||
// Common.KeyboardPanel handles the layer-shell PanelWindow scaffolding
|
||||
// (Exclusive focus on map, screen binding, anchored-to-icon positioning,
|
||||
// outside-click via an overlay MouseArea + Region mask that lets the bar
|
||||
// remain clickable, fade animation, popout coordination). What stays
|
||||
// here is the wifi-specific UI inside.
|
||||
Common.KeyboardPanel {
|
||||
id: panel
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
@@ -255,9 +516,42 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
contentWidth: 340
|
||||
contentHeight: column.implicitHeight + 28
|
||||
|
||||
// Catches all unhandled keys for keyboard navigation. AfterItem priority
|
||||
// lets the passphrase TextField (a child via focus chain) get its keys
|
||||
// first; only events the focused subtree ignores bubble back here.
|
||||
Item {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.priority: Keys.AfterItem
|
||||
Keys.onPressed: function(event) {
|
||||
if (root.passwordSsid !== "") return
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.closePopout()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down || event.text === "j") {
|
||||
root.selectByDelta(1)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up || event.text === "k") {
|
||||
root.selectByDelta(-1)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) {
|
||||
root.activateSelected()
|
||||
event.accepted = true
|
||||
} else if (event.text === "x" || event.text === "X") {
|
||||
root.forgetSelected()
|
||||
event.accepted = true
|
||||
} else if (event.text === "r" || event.text === "R") {
|
||||
root.refresh()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.fill: parent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: 12
|
||||
|
||||
// Header — interface name + type, refresh on the right.
|
||||
@@ -512,81 +806,395 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
}
|
||||
|
||||
// Scrollable network list — cap the height so a busy neighbourhood
|
||||
// doesn't push the popup off-screen. Bluetooth panel uses the same
|
||||
// pattern.
|
||||
Flickable {
|
||||
// doesn't push the popup off-screen. ListView (vs Repeater+Column)
|
||||
// gives us positionViewAtIndex for free, which is what keeps the
|
||||
// keyboard-selected row scrolled into view as j/k walk past the
|
||||
// visible window.
|
||||
ListView {
|
||||
id: networkList
|
||||
visible: root.wifiStationAvailable
|
||||
width: parent.width
|
||||
height: Math.min(networkList.implicitHeight, 240)
|
||||
contentWidth: width
|
||||
contentHeight: networkList.implicitHeight
|
||||
height: Math.min(contentHeight, 240)
|
||||
spacing: 4
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
interactive: contentHeight > height
|
||||
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
||||
Column {
|
||||
id: networkList
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
model: root.wifiStationAvailable ? root.wifiNetworks : []
|
||||
currentIndex: root.selectedIndex
|
||||
onCurrentIndexChanged: if (currentIndex >= 0) positionViewAtIndex(currentIndex, ListView.Contain)
|
||||
|
||||
Repeater {
|
||||
model: root.wifiStationAvailable ? root.wifiNetworks : []
|
||||
|
||||
Common.PillButton {
|
||||
required property var modelData
|
||||
|
||||
width: networkList.width
|
||||
leftAlign: true
|
||||
iconText: root.wifiIconFor(modelData.signal)
|
||||
text: (modelData.ssid || "Hidden") + (modelData.security ? " · " + modelData.security : "")
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 6
|
||||
active: modelData.connected
|
||||
|
||||
onClicked: {
|
||||
if (modelData.connected) return
|
||||
if (actionProc.running) return
|
||||
// Known networks (and open networks) can connect non-interactively
|
||||
// via iwctl. Anything else needs Impala for passphrase entry.
|
||||
var ssid = root.bar.shellQuote(modelData.ssid)
|
||||
var security = root.bar.shellQuote(modelData.security || "")
|
||||
var omarchyPath = root.bar.omarchyPath
|
||||
actionProc.command = ["bash", "-c", `
|
||||
ssid=${ssid}
|
||||
security=${security}
|
||||
station=$(iwctl station list 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g' | awk '/^[[:space:]]*wl/ { print $1; exit }')
|
||||
known=$(iwctl known-networks list 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g' | awk -v s="$ssid" 'NR > 3 {
|
||||
line = $0
|
||||
sub(/^[[:space:]]+/, "", line)
|
||||
sub(/[[:space:]]+$/, "", line)
|
||||
n = split(line, p, /[[:space:]]{2,}/)
|
||||
if (n >= 1 && p[1] == s) { print "yes"; exit }
|
||||
}')
|
||||
if [[ -n $station && ( -n $known || $security == "open" ) ]]; then
|
||||
iwctl --dont-ask station "$station" connect "$ssid"
|
||||
else
|
||||
${omarchyPath}/bin/omarchy-launch-wifi
|
||||
fi
|
||||
`]
|
||||
actionProc.running = true
|
||||
root.popupOpen = false
|
||||
}
|
||||
}
|
||||
// Wrapper takes the required props from ListView's delegate context
|
||||
// (which doesn't bind into nested `component` declarations like
|
||||
// NetworkRow) and passes them down explicitly.
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: ListView.view.width
|
||||
height: row.implicitHeight
|
||||
NetworkRow {
|
||||
id: row
|
||||
width: parent.width
|
||||
net: parent.modelData
|
||||
index: parent.index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
visible: root.wifiStationAvailable
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Open Wi-Fi manager (Impala)"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 6
|
||||
onClicked: { root.bar.run(root.bar.omarchyPath + "/bin/omarchy-launch-wifi"); root.popupOpen = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 a connected row forgets the network.
|
||||
component NetworkRow: Rectangle {
|
||||
id: row
|
||||
required property var net
|
||||
required property int index
|
||||
|
||||
readonly property bool isConnected: net && net.connected
|
||||
readonly property bool isProtected: root.isProtected(net ? net.security : "")
|
||||
readonly property bool isSelected: root.selectedIndex === index
|
||||
// Gate on the matching *Kind/*Reason being non-empty so a hidden-SSID
|
||||
// row (ssid == "") doesn't match the "" defaults of actionSsid etc.
|
||||
readonly property bool isBusy: root.actionKind !== "" && root.actionSsid === (net ? net.ssid : "")
|
||||
readonly property bool isFailed: root.failureReason !== "" && root.failureSsid === (net ? net.ssid : "")
|
||||
readonly property bool isPasswordOpen: root.passwordSsid !== "" && root.passwordSsid === (net ? net.ssid : "")
|
||||
|
||||
readonly property string statusText: {
|
||||
if (!net) return ""
|
||||
if (isBusy && root.actionKind === "connect") return "Connecting…"
|
||||
if (isBusy && root.actionKind === "disconnect") return "Disconnecting…"
|
||||
if (isBusy && root.actionKind === "forget") return "Forgetting…"
|
||||
if (isFailed) return root.failureReason || "Failed"
|
||||
if (isConnected) return "Connected"
|
||||
return ""
|
||||
}
|
||||
|
||||
readonly property color statusColor: {
|
||||
if (isFailed) return root.bar.urgent
|
||||
if (isBusy) return root.bar.foreground
|
||||
if (isConnected) return root.bar.foreground
|
||||
return Qt.darker(root.bar.foreground, 1.5)
|
||||
}
|
||||
|
||||
implicitHeight: rowBody.implicitHeight + (isPasswordOpen ? passwordPanel.implicitHeight + 6 : 0)
|
||||
radius: 4
|
||||
color: isSelected
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.18)
|
||||
: rowMouse.containsMouse
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
: (isConnected ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.06) : "transparent")
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
MouseArea {
|
||||
id: rowMouse
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
height: rowBody.implicitHeight
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: !root.busy
|
||||
|
||||
onClicked: {
|
||||
if (!row.net) return
|
||||
if (row.isConnected) {
|
||||
root.disconnect(row.net.ssid)
|
||||
return
|
||||
}
|
||||
if (row.isProtected) {
|
||||
// Known protected network: iwd has the passphrase, just connect.
|
||||
// Otherwise expand the inline password prompt for this row. Stash
|
||||
// the SSID as a string — if we held a reference to the row
|
||||
// delegate it could be destroyed by a model refresh, and a rapid
|
||||
// second click would overwrite it and misroute the first result.
|
||||
var quotedSsid = root.bar.shellQuote(row.net.ssid)
|
||||
knownCheck.targetSsid = row.net.ssid
|
||||
knownCheck.command = ["bash", "-c", `
|
||||
iwctl known-networks list 2>/dev/null \\
|
||||
| sed -e 's/\\x1b\\[[0-9;]*m//g' \\
|
||||
| awk -v s=${quotedSsid} 'NR > 3 {
|
||||
line = $0
|
||||
sub(/^[[:space:]]+/, "", line)
|
||||
sub(/[[:space:]]+$/, "", line)
|
||||
n = split(line, p, /[[:space:]]{2,}/)
|
||||
if (n >= 1 && p[1] == s) { print "yes"; exit }
|
||||
}'`]
|
||||
knownCheck.running = true
|
||||
return
|
||||
}
|
||||
root.connectKnown(row.net.ssid)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rowBody
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
implicitHeight: Math.max(networkIcon.implicitHeight, networkInfo.implicitHeight, forgetBtn.implicitHeight) + 12
|
||||
|
||||
Text {
|
||||
id: networkIcon
|
||||
text: row.net ? root.wifiIconFor(row.net.signal) : ""
|
||||
color: row.statusColor
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: forgetBtn
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 22
|
||||
height: 22
|
||||
radius: 4
|
||||
visible: row.isConnected
|
||||
color: forgetMouse.containsMouse
|
||||
? Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, root.bar.urgent.b, 0.20)
|
||||
: "transparent"
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: forgetMouse.containsMouse ? root.bar.urgent : Qt.darker(root.bar.foreground, 1.3)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: forgetMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: !root.busy
|
||||
onClicked: if (row.net) root.forget(row.net.ssid)
|
||||
}
|
||||
|
||||
ToolTip {
|
||||
visible: forgetMouse.containsMouse
|
||||
text: "Forget network"
|
||||
delay: 400
|
||||
padding: 0
|
||||
background: Rectangle {
|
||||
color: root.bar.background
|
||||
border.color: root.bar.foreground
|
||||
border.width: 1
|
||||
radius: 0
|
||||
opacity: 0.97
|
||||
}
|
||||
contentItem: Text {
|
||||
text: "Forget network"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
topPadding: 6
|
||||
bottomPadding: 6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shows a lock glyph on the right for protected networks that
|
||||
// aren't currently connected. Once connected, the forget X takes
|
||||
// its place (and 'protected' is implied by the fact we're on it).
|
||||
// Same 22-wide right-anchored centered geometry as forgetBtn so the
|
||||
// glyph centers line up across rows.
|
||||
Text {
|
||||
id: lockIndicator
|
||||
visible: row.isProtected && !row.isConnected
|
||||
width: 22
|
||||
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: 13
|
||||
}
|
||||
|
||||
Column {
|
||||
id: networkInfo
|
||||
spacing: 1
|
||||
anchors.left: networkIcon.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: forgetBtn.visible ? forgetBtn.left
|
||||
: lockIndicator.visible ? lockIndicator.left
|
||||
: parent.right
|
||||
anchors.rightMargin: (forgetBtn.visible || lockIndicator.visible) ? 8 : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: row.net ? (row.net.ssid || "Hidden") : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
// Signal strength is conveyed by the wifi-bars icon and a lock
|
||||
// glyph on the right denotes protected networks, so the second
|
||||
// line only carries action status (Connecting…, Connected,
|
||||
// Failed, etc.). Collapses to zero height when empty so rows
|
||||
// without status keep a tight one-line look.
|
||||
text: row.statusText
|
||||
visible: row.statusText !== ""
|
||||
height: visible ? implicitHeight : 0
|
||||
color: row.statusColor
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inline passphrase prompt — only shown when we hit a protected network
|
||||
// we don't have saved credentials for. Submitting (Enter or the check
|
||||
// button) fires connect; Esc cancels back to the row.
|
||||
Item {
|
||||
id: passwordPanel
|
||||
visible: row.isPasswordOpen
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: rowMouse.bottom
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
anchors.topMargin: 4
|
||||
implicitHeight: pwField.implicitHeight + 8
|
||||
|
||||
TextField {
|
||||
id: pwField
|
||||
anchors.left: parent.left
|
||||
anchors.right: connectPwBtn.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 6
|
||||
echoMode: TextInput.Password
|
||||
placeholderText: "Passphrase"
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
color: root.bar.foreground
|
||||
selectionColor: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.35)
|
||||
selectedTextColor: root.bar.foreground
|
||||
placeholderTextColor: Qt.darker(root.bar.foreground, 1.6)
|
||||
leftPadding: 8
|
||||
rightPadding: 8
|
||||
topPadding: 6
|
||||
bottomPadding: 6
|
||||
enabled: !row.isBusy
|
||||
|
||||
background: Rectangle {
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, pwField.activeFocus ? 0.10 : 0.05)
|
||||
border.color: pwField.activeFocus
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.45)
|
||||
: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.20)
|
||||
border.width: 1
|
||||
radius: 4
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
if (!root.busy && row.net && text.length > 0) root.connectWithPassphrase(row.net.ssid, text)
|
||||
}
|
||||
Keys.onEscapePressed: { root.passwordSsid = ""; text = "" }
|
||||
|
||||
onVisibleChanged: if (visible) Qt.callLater(forceActiveFocus)
|
||||
Component.onCompleted: if (visible) Qt.callLater(forceActiveFocus)
|
||||
}
|
||||
|
||||
// 22×22 right-anchored to line up with forgetBtn and lockIndicator
|
||||
// above. Esc closes the prompt (handled by pwField.Keys.onEscapePressed)
|
||||
// so there's no separate cancel button.
|
||||
Rectangle {
|
||||
id: connectPwBtn
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 22
|
||||
height: 22
|
||||
radius: 4
|
||||
property bool clickEnabled: !root.busy && row.net && pwField.text.length > 0
|
||||
color: connectPwMouse.containsMouse && connectPwBtn.clickEnabled
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.20)
|
||||
: "transparent"
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: connectPwBtn.clickEnabled
|
||||
? (connectPwMouse.containsMouse ? root.bar.foreground : Qt.darker(root.bar.foreground, 1.3))
|
||||
: Qt.darker(root.bar.foreground, 2.0)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectPwMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: connectPwBtn.clickEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (connectPwBtn.clickEnabled) root.connectWithPassphrase(row.net.ssid, pwField.text)
|
||||
}
|
||||
|
||||
ToolTip {
|
||||
visible: connectPwMouse.containsMouse
|
||||
text: "Connect"
|
||||
delay: 400
|
||||
padding: 0
|
||||
background: Rectangle {
|
||||
color: root.bar.background
|
||||
border.color: root.bar.foreground
|
||||
border.width: 1
|
||||
radius: 0
|
||||
opacity: 0.97
|
||||
}
|
||||
contentItem: Text {
|
||||
text: "Connect"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
topPadding: 6
|
||||
bottomPadding: 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One-shot probe: is the just-clicked SSID in iwd's known-networks?
|
||||
// If so, skip the passphrase prompt and connect directly. We hold an SSID
|
||||
// string (not a row delegate) so a model refresh during the probe can't
|
||||
// leave us pointing at a destroyed object. Rows are globally disabled
|
||||
// while `knownCheck.running`, so the SSID can't be overwritten mid-flight.
|
||||
Process {
|
||||
id: knownCheck
|
||||
property string targetSsid: ""
|
||||
stdout: StdioCollector {
|
||||
id: knownStdout
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var ssid = knownCheck.targetSsid
|
||||
knownCheck.targetSsid = ""
|
||||
if (!ssid) return
|
||||
if (text.indexOf("yes") !== -1) root.connectKnown(ssid)
|
||||
else root.passwordSsid = ssid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user