summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorATMDA <atdma2600@gmail.com>2025-11-14 17:42:49 -0500
committerATMDA <atdma2600@gmail.com>2025-11-14 17:42:49 -0500
commit45ef91998e4586dbf16c6ea3db0a9f4e19f4487e (patch)
tree4268a84251bb975731a840c0c9ef2b9136c43625
parentcontrolcenter: minor tidying (capitalization and filename) (diff)
downloadcaelestia-shell-45ef91998e4586dbf16c6ea3db0a9f4e19f4487e.tar.gz
caelestia-shell-45ef91998e4586dbf16c6ea3db0a9f4e19f4487e.tar.bz2
caelestia-shell-45ef91998e4586dbf16c6ea3db0a9f4e19f4487e.zip
tray: wireless password input popout
-rw-r--r--modules/bar/popouts/Content.qml48
-rw-r--r--modules/bar/popouts/Network.qml46
-rw-r--r--modules/bar/popouts/WirelessPasswordPopout.qml599
-rw-r--r--modules/bar/popouts/Wrapper.qml28
4 files changed, 719 insertions, 2 deletions
diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml
index 661a41c..7561eec 100644
--- a/modules/bar/popouts/Content.qml
+++ b/modules/bar/popouts/Content.qml
@@ -32,8 +32,10 @@ Item {
}
Popout {
+ id: networkPopout
name: "network"
sourceComponent: Network {
+ wrapper: root.wrapper
view: "wireless"
}
}
@@ -41,11 +43,57 @@ Item {
Popout {
name: "ethernet"
sourceComponent: Network {
+ wrapper: root.wrapper
view: "ethernet"
}
}
Popout {
+ id: passwordPopout
+ name: "wirelesspassword"
+ sourceComponent: WirelessPasswordPopout {
+ id: passwordComponent
+ wrapper: root.wrapper
+ network: networkPopout.item?.passwordNetwork ?? null
+ }
+
+ Connections {
+ target: root.wrapper
+ function onCurrentNameChanged() {
+ // Update network immediately when password popout becomes active
+ if (root.wrapper.currentName === "wirelesspassword") {
+ // Set network immediately if available
+ if (networkPopout.item && networkPopout.item.passwordNetwork) {
+ if (passwordPopout.item) {
+ passwordPopout.item.network = networkPopout.item.passwordNetwork;
+ }
+ }
+ // Also try after a short delay in case networkPopout.item wasn't ready
+ Qt.callLater(() => {
+ if (passwordPopout.item && networkPopout.item && networkPopout.item.passwordNetwork) {
+ passwordPopout.item.network = networkPopout.item.passwordNetwork;
+ }
+ }, 100);
+ }
+ }
+ }
+
+ Connections {
+ target: networkPopout
+ function onItemChanged() {
+ // When network popout loads, update password popout if it's active
+ if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) {
+ Qt.callLater(() => {
+ if (networkPopout.item && networkPopout.item.passwordNetwork) {
+ passwordPopout.item.network = networkPopout.item.passwordNetwork;
+ }
+ });
+ }
+ }
+ }
+ }
+
+ Popout {
name: "bluetooth"
sourceComponent: Bluetooth {
wrapper: root.wrapper
diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml
index b807ce4..93ff867 100644
--- a/modules/bar/popouts/Network.qml
+++ b/modules/bar/popouts/Network.qml
@@ -12,8 +12,12 @@ import QtQuick.Layouts
ColumnLayout {
id: root
+ required property Item wrapper
+
property string connectingToSsid: ""
property string view: "wireless" // "wireless" or "ethernet"
+ property var passwordNetwork: null
+ property bool showPasswordDialog: false
spacing: Appearance.spacing.small
width: Config.bar.sizes.networkWidth
@@ -129,7 +133,27 @@ ColumnLayout {
Nmcli.disconnectFromNetwork();
} else {
root.connectingToSsid = networkItem.modelData.ssid;
- Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null);
+ // Check if network is secure
+ if (networkItem.modelData.isSecure) {
+ // Try to connect first - will show password dialog if password is needed
+ Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, (result) => {
+ if (result && result.needsPassword) {
+ // Password is required - show password dialog
+ root.passwordNetwork = networkItem.modelData;
+ root.showPasswordDialog = true;
+ root.wrapper.currentName = "wirelesspassword";
+ } else if (result && result.success) {
+ // Connection successful with saved password
+ root.connectingToSsid = "";
+ } else {
+ // Connection failed for other reasons
+ root.connectingToSsid = "";
+ }
+ });
+ } else {
+ // Open network, no password needed
+ Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null);
+ }
}
}
}
@@ -329,6 +353,15 @@ ColumnLayout {
function onActiveChanged(): void {
if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) {
root.connectingToSsid = "";
+ // Close password dialog if we successfully connected
+ if (root.showPasswordDialog && root.passwordNetwork &&
+ Nmcli.active.ssid === root.passwordNetwork.ssid) {
+ root.showPasswordDialog = false;
+ root.passwordNetwork = null;
+ if (root.wrapper.currentName === "wirelesspassword") {
+ root.wrapper.currentName = "network";
+ }
+ }
}
}
@@ -338,6 +371,17 @@ ColumnLayout {
}
}
+ Connections {
+ target: root.wrapper
+ function onCurrentNameChanged(): void {
+ // Clear password network when leaving password dialog
+ if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) {
+ root.showPasswordDialog = false;
+ root.passwordNetwork = null;
+ }
+ }
+ }
+
component Toggle: RowLayout {
required property string label
property alias checked: toggle.checked
diff --git a/modules/bar/popouts/WirelessPasswordPopout.qml b/modules/bar/popouts/WirelessPasswordPopout.qml
new file mode 100644
index 0000000..80bdd10
--- /dev/null
+++ b/modules/bar/popouts/WirelessPasswordPopout.qml
@@ -0,0 +1,599 @@
+pragma ComponentBehavior: Bound
+
+import qs.components
+import qs.components.controls
+import qs.services
+import qs.config
+import Quickshell
+import QtQuick
+import QtQuick.Layouts
+
+ColumnLayout {
+ id: root
+
+ required property Item wrapper
+ property var network: null
+ property bool isClosing: false
+
+ readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword"
+
+ Connections {
+ target: root.wrapper
+ function onCurrentNameChanged() {
+ if (root.wrapper.currentName === "wirelesspassword") {
+ // Update network when popout becomes active
+ Qt.callLater(() => {
+ // Try to get network from parent Content's networkPopout
+ const content = root.parent?.parent?.parent;
+ if (content) {
+ const networkPopout = content.children.find(c => c.name === "network");
+ if (networkPopout && networkPopout.item) {
+ root.network = networkPopout.item.passwordNetwork;
+ }
+ }
+ // Force focus to password container when popout becomes active
+ Qt.callLater(() => {
+ passwordContainer.forceActiveFocus();
+ }, 100);
+ }, 100);
+ }
+ }
+ }
+
+ spacing: Appearance.spacing.normal
+
+ implicitWidth: 400
+ implicitHeight: content.implicitHeight + Appearance.padding.large * 2
+
+ visible: shouldBeVisible || isClosing
+ enabled: shouldBeVisible && !isClosing
+ focus: enabled
+
+ Component.onCompleted: {
+ if (shouldBeVisible) {
+ Qt.callLater(() => {
+ root.forceActiveFocus();
+ passwordContainer.forceActiveFocus();
+ }, 150);
+ }
+ }
+
+ onShouldBeVisibleChanged: {
+ if (shouldBeVisible) {
+ Qt.callLater(() => {
+ root.forceActiveFocus();
+ passwordContainer.forceActiveFocus();
+ }, 150);
+ }
+ }
+
+ Keys.onEscapePressed: {
+ closeDialog();
+ }
+
+ StyledRect {
+ Layout.fillWidth: true
+ Layout.preferredWidth: 400
+ implicitHeight: content.implicitHeight + Appearance.padding.large * 2
+
+ radius: Appearance.rounding.normal
+ color: Colours.tPalette.m3surface
+ opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0
+ scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ Behavior on scale {
+ Anim {}
+ }
+
+ ParallelAnimation {
+ running: root.isClosing
+ onFinished: {
+ if (root.isClosing) {
+ root.isClosing = false;
+ }
+ }
+
+ Anim {
+ target: parent
+ property: "opacity"
+ to: 0
+ }
+ Anim {
+ target: parent
+ property: "scale"
+ to: 0.7
+ }
+ }
+
+ Keys.onEscapePressed: closeDialog();
+
+ ColumnLayout {
+ id: content
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Appearance.padding.large
+
+ spacing: Appearance.spacing.normal
+
+ MaterialIcon {
+ Layout.alignment: Qt.AlignHCenter
+ text: "lock"
+ font.pointSize: Appearance.font.size.extraLarge * 2
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ text: qsTr("Enter password")
+ font.pointSize: Appearance.font.size.large
+ font.weight: 500
+ }
+
+ StyledText {
+ id: networkNameText
+ Layout.alignment: Qt.AlignHCenter
+ text: {
+ if (root.network) {
+ const ssid = root.network.ssid;
+ if (ssid && ssid.length > 0) {
+ return qsTr("Network: %1").arg(ssid);
+ }
+ }
+ return qsTr("Network: Unknown");
+ }
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.small
+ }
+
+ Timer {
+ interval: 50
+ running: root.shouldBeVisible && (!root.network || !root.network.ssid)
+ repeat: true
+ property int attempts: 0
+ onTriggered: {
+ attempts++;
+ // Keep trying to get network from Network component
+ const content = root.parent?.parent?.parent;
+ if (content) {
+ const networkPopout = content.children.find(c => c.name === "network");
+ if (networkPopout && networkPopout.item && networkPopout.item.passwordNetwork) {
+ root.network = networkPopout.item.passwordNetwork;
+ }
+ }
+ // Stop if we got it or after 20 attempts (1 second)
+ if ((root.network && root.network.ssid) || attempts >= 20) {
+ stop();
+ attempts = 0;
+ }
+ }
+ onRunningChanged: {
+ if (!running) {
+ attempts = 0;
+ }
+ }
+ }
+
+ StyledText {
+ id: statusText
+
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: Appearance.spacing.small
+ visible: connectButton.connecting
+ text: {
+ if (connectButton.connecting) {
+ return qsTr("Connecting...");
+ }
+ return "";
+ }
+ color: Colours.palette.m3onSurfaceVariant
+ font.pointSize: Appearance.font.size.small
+ font.weight: 400
+ wrapMode: Text.WordWrap
+ Layout.maximumWidth: parent.width - Appearance.padding.large * 2
+ }
+
+ FocusScope {
+ id: passwordContainer
+ objectName: "passwordContainer"
+ Layout.topMargin: Appearance.spacing.large
+ Layout.fillWidth: true
+ implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2)
+
+ focus: true
+ activeFocusOnTab: true
+ Keys.onPressed: event => {
+ if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
+ if (connectButton.enabled) {
+ connectButton.clicked();
+ }
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Backspace) {
+ if (event.modifiers & Qt.ControlModifier) {
+ passwordBuffer = "";
+ } else {
+ passwordBuffer = passwordBuffer.slice(0, -1);
+ }
+ event.accepted = true;
+ } else if (event.text && event.text.length > 0) {
+ passwordBuffer += event.text;
+ event.accepted = true;
+ }
+ }
+
+ property string passwordBuffer: ""
+
+ Connections {
+ target: root
+ function onShouldBeVisibleChanged(): void {
+ if (root.shouldBeVisible) {
+ Qt.callLater(() => {
+ passwordContainer.forceActiveFocus();
+ }, 50);
+ passwordContainer.passwordBuffer = "";
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ if (root.shouldBeVisible) {
+ Qt.callLater(() => {
+ passwordContainer.forceActiveFocus();
+ }, 100);
+ }
+ }
+
+ StyledRect {
+ anchors.fill: parent
+ radius: Appearance.rounding.normal
+ color: Colours.tPalette.m3surfaceContainer
+ border.width: passwordContainer.activeFocus ? 2 : 1
+ border.color: passwordContainer.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline
+
+ Behavior on border.color {
+ CAnim {}
+ }
+ }
+
+ StateLayer {
+ hoverEnabled: false
+ cursorShape: Qt.IBeamCursor
+
+ function onClicked(): void {
+ passwordContainer.forceActiveFocus();
+ }
+ }
+
+ StyledText {
+ id: placeholder
+
+ anchors.centerIn: parent
+ text: qsTr("Password")
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.normal
+ font.family: Appearance.font.family.mono
+ opacity: passwordContainer.passwordBuffer ? 0 : 1
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+
+ ListView {
+ id: charList
+
+ readonly property int fullWidth: count * (implicitHeight + spacing) - spacing
+
+ anchors.centerIn: parent
+ implicitWidth: fullWidth
+ implicitHeight: Appearance.font.size.normal
+
+ orientation: Qt.Horizontal
+ spacing: Appearance.spacing.small / 2
+ interactive: false
+
+ model: ScriptModel {
+ values: passwordContainer.passwordBuffer.split("")
+ }
+
+ delegate: StyledRect {
+ id: ch
+
+ implicitWidth: implicitHeight
+ implicitHeight: charList.implicitHeight
+
+ color: Colours.palette.m3onSurface
+ radius: Appearance.rounding.small / 2
+
+ opacity: 0
+ scale: 0
+ Component.onCompleted: {
+ opacity = 1;
+ scale = 1;
+ }
+ ListView.onRemove: removeAnim.start()
+
+ SequentialAnimation {
+ id: removeAnim
+
+ PropertyAction {
+ target: ch
+ property: "ListView.delayRemove"
+ value: true
+ }
+ ParallelAnimation {
+ Anim {
+ target: ch
+ property: "opacity"
+ to: 0
+ }
+ Anim {
+ target: ch
+ property: "scale"
+ to: 0.5
+ }
+ }
+ PropertyAction {
+ target: ch
+ property: "ListView.delayRemove"
+ value: false
+ }
+ }
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ Behavior on scale {
+ Anim {
+ duration: Appearance.anim.durations.expressiveFastSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
+ }
+ }
+ }
+
+ Behavior on implicitWidth {
+ Anim {}
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.topMargin: Appearance.spacing.normal
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.normal
+
+ SimpleButton {
+ id: cancelButton
+
+ Layout.fillWidth: true
+ Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
+ color: Colours.palette.m3secondaryContainer
+ onColor: Colours.palette.m3onSecondaryContainer
+ text: qsTr("Cancel")
+
+ onClicked: closeDialog();
+ }
+
+ SimpleButton {
+ id: connectButton
+
+ Layout.fillWidth: true
+ Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
+ color: Colours.palette.m3primary
+ onColor: Colours.palette.m3onPrimary
+ text: qsTr("Connect")
+ enabled: passwordContainer.passwordBuffer.length > 0 && !connecting
+
+ property bool connecting: false
+
+ onClicked: {
+ if (!root.network || connecting) {
+ return;
+ }
+
+ const password = passwordContainer.passwordBuffer;
+ if (!password || password.length === 0) {
+ return;
+ }
+
+ // Set connecting state
+ connecting = true;
+ enabled = false;
+ text = qsTr("Connecting...");
+
+ // Connect to network
+ Nmcli.connectToNetwork(
+ root.network.ssid,
+ password,
+ root.network.bssid || "",
+ (result) => {
+ if (result && result.success) {
+ // Connection successful, monitor will handle the rest
+ } else if (result && result.needsPassword) {
+ // Shouldn't happen since we provided password
+ connectionMonitor.stop();
+ connecting = false;
+ enabled = true;
+ text = qsTr("Connect");
+ } else {
+ // Connection failed immediately - return to network popout
+ connectionMonitor.stop();
+ connecting = false;
+ enabled = true;
+ text = qsTr("Connect");
+ Qt.callLater(() => {
+ if (root.wrapper.currentName === "wirelesspassword") {
+ root.wrapper.currentName = "network";
+ }
+ }, 500);
+ }
+ }
+ );
+
+ // Start monitoring connection
+ connectionMonitor.start();
+ }
+ }
+ }
+ }
+ }
+
+ function checkConnectionStatus(): void {
+ if (!root.shouldBeVisible || !connectButton.connecting) {
+ return;
+ }
+
+ // Check if we're connected to the target network (case-insensitive SSID comparison)
+ const isConnected = root.network && Nmcli.active && Nmcli.active.ssid &&
+ Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
+
+ if (isConnected) {
+ // Successfully connected - give it a moment for network list to update
+ Qt.callLater(() => {
+ // Double-check connection is still active
+ if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) {
+ const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
+ if (stillConnected) {
+ connectionMonitor.stop();
+ connectButton.connecting = false;
+ connectButton.text = qsTr("Connect");
+ // Return to network popout on successful connection
+ if (root.wrapper.currentName === "wirelesspassword") {
+ root.wrapper.currentName = "network";
+ }
+ closeDialog();
+ }
+ }
+ }, 500);
+ return;
+ }
+
+ // Check for connection failures - if pending connection was cleared but we're not connected
+ if (Nmcli.pendingConnection === null && connectButton.connecting) {
+ // Wait a bit more before giving up (allow time for connection to establish)
+ if (connectionMonitor.repeatCount > 10) {
+ connectionMonitor.stop();
+ connectButton.connecting = false;
+ connectButton.enabled = true;
+ connectButton.text = qsTr("Connect");
+ // Return to network popout on timeout failure
+ Qt.callLater(() => {
+ if (root.wrapper.currentName === "wirelesspassword") {
+ root.wrapper.currentName = "network";
+ }
+ }, 500);
+ }
+ }
+ }
+
+ Timer {
+ id: connectionMonitor
+ interval: 1000
+ repeat: true
+ triggeredOnStart: false
+ property int repeatCount: 0
+
+ onTriggered: {
+ repeatCount++;
+ checkConnectionStatus();
+ }
+
+ onRunningChanged: {
+ if (!running) {
+ repeatCount = 0;
+ }
+ }
+ }
+
+ Connections {
+ target: Nmcli
+ function onActiveChanged() {
+ if (root.shouldBeVisible) {
+ checkConnectionStatus();
+ }
+ }
+ function onConnectionFailed(ssid: string) {
+ if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) {
+ connectionMonitor.stop();
+ connectButton.connecting = false;
+ connectButton.enabled = true;
+ connectButton.text = qsTr("Connect");
+ // Return to network popout on connection failure
+ Qt.callLater(() => {
+ if (root.wrapper.currentName === "wirelesspassword") {
+ root.wrapper.currentName = "network";
+ }
+ }, 500);
+ }
+ }
+ }
+
+ function closeDialog(): void {
+ if (isClosing) {
+ return;
+ }
+
+ isClosing = true;
+ passwordContainer.passwordBuffer = "";
+ connectButton.connecting = false;
+ connectButton.text = qsTr("Connect");
+ connectionMonitor.stop();
+
+ // Return to network popout
+ if (root.wrapper.currentName === "wirelesspassword") {
+ root.wrapper.currentName = "network";
+ }
+ }
+
+ component SimpleButton: StyledRect {
+ id: button
+
+ property color onColor: Colours.palette.m3onSurface
+ property alias disabled: stateLayer.disabled
+ property alias text: label.text
+ property alias enabled: stateLayer.enabled
+ property string icon: ""
+
+ implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2
+ implicitHeight: rowLayout.implicitHeight + Appearance.padding.small
+ radius: Appearance.rounding.normal
+
+ StateLayer {
+ id: stateLayer
+ color: parent.onColor
+ function onClicked(): void {
+ if (parent.enabled !== false) {
+ parent.clicked();
+ }
+ }
+ }
+
+ RowLayout {
+ id: rowLayout
+ anchors.centerIn: parent
+ spacing: Appearance.spacing.small
+
+ MaterialIcon {
+ id: iconItem
+ visible: button.icon.length > 0
+ text: button.icon
+ color: button.onColor
+ font.pointSize: Appearance.font.size.large
+ }
+
+ StyledText {
+ id: label
+ Layout.leftMargin: button.icon.length > 0 ? Appearance.padding.smaller : 0
+ color: parent.parent.onColor
+ }
+ }
+
+ signal clicked
+ }
+}
+
diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml
index 4be47e4..5ef4f9d 100644
--- a/modules/bar/popouts/Wrapper.qml
+++ b/modules/bar/popouts/Wrapper.qml
@@ -55,7 +55,25 @@ Item {
implicitWidth: nonAnimWidth
implicitHeight: nonAnimHeight
- Keys.onEscapePressed: close()
+ focus: hasCurrent
+ Keys.onEscapePressed: {
+ // Forward escape to password popout if active, otherwise close
+ if (currentName === "wirelesspassword" && content.item) {
+ const passwordPopout = content.item.children.find(c => c.name === "wirelesspassword");
+ if (passwordPopout && passwordPopout.item) {
+ passwordPopout.item.closeDialog();
+ return;
+ }
+ }
+ close();
+ }
+
+ Keys.onPressed: event => {
+ // Don't intercept keys when password popout is active - let it handle them
+ if (currentName === "wirelesspassword") {
+ event.accepted = false;
+ }
+ }
HyprlandFocusGrab {
active: root.isDetached
@@ -70,6 +88,14 @@ Item {
property: "WlrLayershell.keyboardFocus"
value: WlrKeyboardFocus.OnDemand
}
+
+ Binding {
+ when: root.hasCurrent && root.currentName === "wirelesspassword"
+
+ target: QsWindow.window
+ property: "WlrLayershell.keyboardFocus"
+ value: WlrKeyboardFocus.OnDemand
+ }
Comp {
id: content