summaryrefslogtreecommitdiff
path: root/modules/bar
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2026-01-03 17:53:06 +1100
committerGitHub <noreply@github.com>2026-01-03 17:53:06 +1100
commitbdcd13222fc6edc77c779a396900ab909e7d5439 (patch)
treef9457f3c91c05ec852f974f239d06aca52a3918e /modules/bar
parent[CI] chore: update flake (diff)
parentMerge branch 'caelestia-dots:main' into main (diff)
downloadcaelestia-shell-bdcd13222fc6edc77c779a396900ab909e7d5439.tar.gz
caelestia-shell-bdcd13222fc6edc77c779a396900ab909e7d5439.tar.bz2
caelestia-shell-bdcd13222fc6edc77c779a396900ab909e7d5439.zip
Merge pull request #906 from atdma/main
controlcenter: many setting panes and minor features
Diffstat (limited to 'modules/bar')
-rw-r--r--modules/bar/components/OsIcon.qml26
-rw-r--r--modules/bar/components/Settings.qml44
-rw-r--r--modules/bar/components/SettingsIcon.qml44
-rw-r--r--modules/bar/components/StatusIcons.qml14
-rw-r--r--modules/bar/components/Tray.qml2
-rw-r--r--modules/bar/popouts/Audio.qml44
-rw-r--r--modules/bar/popouts/Bluetooth.qml45
-rw-r--r--modules/bar/popouts/Content.qml59
-rw-r--r--modules/bar/popouts/Network.qml211
-rw-r--r--modules/bar/popouts/WirelessPassword.qml606
-rw-r--r--modules/bar/popouts/Wrapper.qml28
11 files changed, 1022 insertions, 101 deletions
diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml
index ed6da5d..2bc3864 100644
--- a/modules/bar/components/OsIcon.qml
+++ b/modules/bar/components/OsIcon.qml
@@ -2,9 +2,27 @@ import qs.components.effects
import qs.services
import qs.config
import qs.utils
+import QtQuick
-ColouredIcon {
- source: SysInfo.osLogo
- implicitSize: Appearance.font.size.large * 1.2
- colour: Colours.palette.m3tertiary
+Item {
+ id: root
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ const visibilities = Visibilities.getForActive();
+ visibilities.launcher = !visibilities.launcher;
+ }
+ }
+
+ ColouredIcon {
+ anchors.centerIn: parent
+ source: SysInfo.osLogo
+ implicitSize: Appearance.font.size.large * 1.2
+ colour: Colours.palette.m3tertiary
+ }
+
+ implicitWidth: Appearance.font.size.large * 1.2
+ implicitHeight: Appearance.font.size.large * 1.2
}
diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml
new file mode 100644
index 0000000..7cd18be
--- /dev/null
+++ b/modules/bar/components/Settings.qml
@@ -0,0 +1,44 @@
+import qs.components
+import qs.modules.controlcenter
+import qs.services
+import qs.config
+import Quickshell
+import QtQuick
+
+Item {
+ id: root
+
+ implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
+ implicitHeight: icon.implicitHeight
+
+ StateLayer {
+ // Cursed workaround to make the height larger than the parent
+ anchors.fill: undefined
+ anchors.centerIn: parent
+ implicitWidth: implicitHeight
+ implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
+
+ radius: Appearance.rounding.full
+
+ function onClicked(): void {
+ WindowFactory.create(null, {
+ active: "network"
+ });
+ }
+ }
+
+ MaterialIcon {
+ id: icon
+
+ anchors.centerIn: parent
+ anchors.horizontalCenterOffset: -1
+
+ text: "settings"
+ color: Colours.palette.m3onSurface
+ font.bold: true
+ font.pointSize: Appearance.font.size.normal
+ }
+}
+
+
+
diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml
new file mode 100644
index 0000000..7cd18be
--- /dev/null
+++ b/modules/bar/components/SettingsIcon.qml
@@ -0,0 +1,44 @@
+import qs.components
+import qs.modules.controlcenter
+import qs.services
+import qs.config
+import Quickshell
+import QtQuick
+
+Item {
+ id: root
+
+ implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
+ implicitHeight: icon.implicitHeight
+
+ StateLayer {
+ // Cursed workaround to make the height larger than the parent
+ anchors.fill: undefined
+ anchors.centerIn: parent
+ implicitWidth: implicitHeight
+ implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
+
+ radius: Appearance.rounding.full
+
+ function onClicked(): void {
+ WindowFactory.create(null, {
+ active: "network"
+ });
+ }
+ }
+
+ MaterialIcon {
+ id: icon
+
+ anchors.centerIn: parent
+ anchors.horizontalCenterOffset: -1
+
+ text: "settings"
+ color: Colours.palette.m3onSurface
+ font.bold: true
+ font.pointSize: Appearance.font.size.normal
+ }
+}
+
+
+
diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml
index 27fd9f8..2f52596 100644
--- a/modules/bar/components/StatusIcons.qml
+++ b/modules/bar/components/StatusIcons.qml
@@ -147,7 +147,19 @@ StyledRect {
sourceComponent: MaterialIcon {
animate: true
- text: Network.active ? Icons.getNetworkIcon(Network.active.strength ?? 0) : "wifi_off"
+ text: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off"
+ color: root.colour
+ }
+ }
+
+ // Ethernet icon
+ WrappedLoader {
+ name: "ethernet"
+ active: Config.bar.status.showNetwork && Nmcli.activeEthernet
+
+ sourceComponent: MaterialIcon {
+ animate: true
+ text: "cable"
color: root.colour
}
}
diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml
index efd0c3a..96956f6 100644
--- a/modules/bar/components/Tray.qml
+++ b/modules/bar/components/Tray.qml
@@ -30,7 +30,7 @@ StyledRect {
implicitWidth: Config.bar.sizes.innerWidth
implicitHeight: nonAnimHeight
- color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.tray.background ? Colours.tPalette.m3surfaceContainer.a : 0)
+ color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.full
Column {
diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml
index 952dd6b..58b29ba 100644
--- a/modules/bar/popouts/Audio.qml
+++ b/modules/bar/popouts/Audio.qml
@@ -9,6 +9,7 @@ import Quickshell.Services.Pipewire
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
+import "../../controlcenter/network"
Item {
id: root
@@ -104,43 +105,16 @@ Item {
}
}
- StyledRect {
+ IconTextButton {
+ Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.normal
- visible: Config.general.apps.audio.length > 0
-
- implicitWidth: expandBtn.implicitWidth + Appearance.padding.normal * 2
- implicitHeight: expandBtn.implicitHeight + Appearance.padding.small
-
- radius: Appearance.rounding.normal
- color: Colours.palette.m3primaryContainer
-
- StateLayer {
- color: Colours.palette.m3onPrimaryContainer
-
- function onClicked(): void {
- root.wrapper.hasCurrent = false;
- Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.audio]);
- }
- }
+ inactiveColour: Colours.palette.m3primaryContainer
+ inactiveOnColour: Colours.palette.m3onPrimaryContainer
+ verticalPadding: Appearance.padding.small
+ text: qsTr("Open settings")
+ icon: "settings"
- RowLayout {
- id: expandBtn
-
- anchors.centerIn: parent
- spacing: Appearance.spacing.small
-
- StyledText {
- Layout.leftMargin: Appearance.padding.smaller
- text: qsTr("Open settings")
- color: Colours.palette.m3onPrimaryContainer
- }
-
- MaterialIcon {
- text: "chevron_right"
- color: Colours.palette.m3onPrimaryContainer
- font.pointSize: Appearance.font.size.large
- }
- }
+ onClicked: root.wrapper.detach("audio")
}
}
}
diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml
index 53d8b29..4674905 100644
--- a/modules/bar/popouts/Bluetooth.qml
+++ b/modules/bar/popouts/Bluetooth.qml
@@ -9,6 +9,7 @@ import Quickshell
import Quickshell.Bluetooth
import QtQuick
import QtQuick.Layouts
+import "../../controlcenter/network"
ColumnLayout {
id: root
@@ -20,7 +21,7 @@ ColumnLayout {
StyledText {
Layout.topMargin: Appearance.padding.normal
Layout.rightMargin: Appearance.padding.small
- text: qsTr("Bluetooth %1").arg(BluetoothAdapterState.toString(Bluetooth.defaultAdapter?.state).toLowerCase())
+ text: qsTr("Bluetooth")
font.weight: 500
}
@@ -164,40 +165,16 @@ ColumnLayout {
}
}
- StyledRect {
- Layout.topMargin: Appearance.spacing.small
- implicitWidth: expandBtn.implicitWidth + Appearance.padding.normal * 2
- implicitHeight: expandBtn.implicitHeight + Appearance.padding.small
-
- radius: Appearance.rounding.normal
- color: Colours.palette.m3primaryContainer
-
- StateLayer {
- color: Colours.palette.m3onPrimaryContainer
-
- function onClicked(): void {
- root.wrapper.detach("bluetooth");
- }
- }
-
- RowLayout {
- id: expandBtn
-
- anchors.centerIn: parent
- spacing: Appearance.spacing.small
-
- StyledText {
- Layout.leftMargin: Appearance.padding.smaller
- text: qsTr("Open panel")
- color: Colours.palette.m3onPrimaryContainer
- }
+ IconTextButton {
+ Layout.fillWidth: true
+ Layout.topMargin: Appearance.spacing.normal
+ inactiveColour: Colours.palette.m3primaryContainer
+ inactiveOnColour: Colours.palette.m3onPrimaryContainer
+ verticalPadding: Appearance.padding.small
+ text: qsTr("Open settings")
+ icon: "settings"
- MaterialIcon {
- text: "chevron_right"
- color: Colours.palette.m3onPrimaryContainer
- font.pointSize: Appearance.font.size.large
- }
- }
+ onClicked: root.wrapper.detach("bluetooth")
}
component Toggle: RowLayout {
diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml
index e3f569d..da993fa 100644
--- a/modules/bar/popouts/Content.qml
+++ b/modules/bar/popouts/Content.qml
@@ -32,8 +32,65 @@ Item {
}
Popout {
+ id: networkPopout
name: "network"
- sourceComponent: Network {}
+ sourceComponent: Network {
+ wrapper: root.wrapper
+ view: "wireless"
+ }
+ }
+
+ Popout {
+ name: "ethernet"
+ sourceComponent: Network {
+ wrapper: root.wrapper
+ view: "ethernet"
+ }
+ }
+
+ Popout {
+ id: passwordPopout
+ name: "wirelesspassword"
+ sourceComponent: WirelessPassword {
+ 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 {
diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml
index f21a92d..0e99613 100644
--- a/modules/bar/popouts/Network.qml
+++ b/modules/bar/popouts/Network.qml
@@ -12,35 +12,48 @@ 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
+ // Wireless section
StyledText {
- Layout.topMargin: Appearance.padding.normal
+ visible: root.view === "wireless"
+ Layout.preferredHeight: visible ? implicitHeight : 0
+ Layout.topMargin: visible ? Appearance.padding.normal : 0
Layout.rightMargin: Appearance.padding.small
- text: qsTr("Wifi %1").arg(Network.wifiEnabled ? "enabled" : "disabled")
+ text: qsTr("Wireless")
font.weight: 500
}
Toggle {
+ visible: root.view === "wireless"
+ Layout.preferredHeight: visible ? implicitHeight : 0
label: qsTr("Enabled")
- checked: Network.wifiEnabled
- toggle.onToggled: Network.enableWifi(checked)
+ checked: Nmcli.wifiEnabled
+ toggle.onToggled: Nmcli.enableWifi(checked)
}
StyledText {
- Layout.topMargin: Appearance.spacing.small
+ visible: root.view === "wireless"
+ Layout.preferredHeight: visible ? implicitHeight : 0
+ Layout.topMargin: visible ? Appearance.spacing.small : 0
Layout.rightMargin: Appearance.padding.small
- text: qsTr("%1 networks available").arg(Network.networks.length)
+ text: qsTr("%1 networks available").arg(Nmcli.networks.length)
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
Repeater {
+ visible: root.view === "wireless"
model: ScriptModel {
- values: [...Network.networks].sort((a, b) => {
+ values: [...Nmcli.networks].sort((a, b) => {
if (a.active !== b.active)
return b.active - a.active;
return b.strength - a.strength;
@@ -50,10 +63,12 @@ ColumnLayout {
RowLayout {
id: networkItem
- required property Network.AccessPoint modelData
+ required property Nmcli.AccessPoint modelData
readonly property bool isConnecting: root.connectingToSsid === modelData.ssid
readonly property bool loading: networkItem.isConnecting
+ visible: root.view === "wireless"
+ Layout.preferredHeight: visible ? implicitHeight : 0
Layout.fillWidth: true
Layout.rightMargin: Appearance.padding.small
spacing: Appearance.spacing.small
@@ -96,10 +111,8 @@ ColumnLayout {
}
StyledRect {
- id: connectBtn
-
implicitWidth: implicitHeight
- implicitHeight: connectIcon.implicitHeight + Appearance.padding.small
+ implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.small
radius: Appearance.rounding.full
color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0)
@@ -111,20 +124,32 @@ ColumnLayout {
StateLayer {
color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
- disabled: networkItem.loading || !Network.wifiEnabled
+ disabled: networkItem.loading || !Nmcli.wifiEnabled
function onClicked(): void {
if (networkItem.modelData.active) {
- Network.disconnectFromNetwork();
+ Nmcli.disconnectFromNetwork();
} else {
root.connectingToSsid = networkItem.modelData.ssid;
- Network.connectToNetwork(networkItem.modelData.ssid, "");
+ NetworkConnection.handleConnect(
+ networkItem.modelData,
+ null,
+ (network) => {
+ // Password is required - show password dialog
+ root.passwordNetwork = network;
+ root.showPasswordDialog = true;
+ root.wrapper.currentName = "wirelesspassword";
+ }
+ );
+
+ // Clear connecting state if connection succeeds immediately (saved profile)
+ // This is handled by the onActiveChanged connection below
}
}
}
MaterialIcon {
- id: connectIcon
+ id: wirelessConnectIcon
anchors.centerIn: parent
animate: true
@@ -142,7 +167,9 @@ ColumnLayout {
}
StyledRect {
- Layout.topMargin: Appearance.spacing.small
+ visible: root.view === "wireless"
+ Layout.preferredHeight: visible ? implicitHeight : 0
+ Layout.topMargin: visible ? Appearance.spacing.small : 0
Layout.fillWidth: true
implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small * 2
@@ -151,10 +178,10 @@ ColumnLayout {
StateLayer {
color: Colours.palette.m3onPrimaryContainer
- disabled: Network.scanning || !Network.wifiEnabled
+ disabled: Nmcli.scanning || !Nmcli.wifiEnabled
function onClicked(): void {
- Network.rescanWifi();
+ Nmcli.rescanWifi();
}
}
@@ -163,17 +190,19 @@ ColumnLayout {
anchors.centerIn: parent
spacing: Appearance.spacing.small
- opacity: Network.scanning ? 0 : 1
+ opacity: Nmcli.scanning ? 0 : 1
MaterialIcon {
id: scanIcon
+ Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575)
animate: true
text: "wifi_find"
color: Colours.palette.m3onPrimaryContainer
}
StyledText {
+ Layout.topMargin: -Math.round(scanIcon.fontInfo.pointSize * 0.0575)
text: qsTr("Rescan networks")
color: Colours.palette.m3onPrimaryContainer
}
@@ -188,26 +217,160 @@ ColumnLayout {
strokeWidth: Appearance.padding.small / 2
bgColour: "transparent"
implicitHeight: parent.implicitHeight - Appearance.padding.smaller * 2
- running: Network.scanning
+ running: Nmcli.scanning
+ }
+ }
+
+ // Ethernet section
+ StyledText {
+ visible: root.view === "ethernet"
+ Layout.preferredHeight: visible ? implicitHeight : 0
+ Layout.topMargin: visible ? Appearance.padding.normal : 0
+ Layout.rightMargin: Appearance.padding.small
+ text: qsTr("Ethernet")
+ font.weight: 500
+ }
+
+ StyledText {
+ visible: root.view === "ethernet"
+ Layout.preferredHeight: visible ? implicitHeight : 0
+ Layout.topMargin: visible ? Appearance.spacing.small : 0
+ Layout.rightMargin: Appearance.padding.small
+ text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length)
+ color: Colours.palette.m3onSurfaceVariant
+ font.pointSize: Appearance.font.size.small
+ }
+
+ Repeater {
+ visible: root.view === "ethernet"
+ model: ScriptModel {
+ values: [...Nmcli.ethernetDevices].sort((a, b) => {
+ if (a.connected !== b.connected)
+ return b.connected - a.connected;
+ return (a.interface || "").localeCompare(b.interface || "");
+ }).slice(0, 8)
+ }
+
+ RowLayout {
+ id: ethernetItem
+
+ required property var modelData
+ readonly property bool loading: false
+
+ visible: root.view === "ethernet"
+ Layout.preferredHeight: visible ? implicitHeight : 0
+ Layout.fillWidth: true
+ Layout.rightMargin: Appearance.padding.small
+ spacing: Appearance.spacing.small
+
+ opacity: 0
+ scale: 0.7
+
+ Component.onCompleted: {
+ opacity = 1;
+ scale = 1;
+ }
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ Behavior on scale {
+ Anim {}
+ }
+
+ MaterialIcon {
+ text: "cable"
+ color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
+ }
+
+ StyledText {
+ Layout.leftMargin: Appearance.spacing.small / 2
+ Layout.rightMargin: Appearance.spacing.small / 2
+ Layout.fillWidth: true
+ text: ethernetItem.modelData.interface || qsTr("Unknown")
+ elide: Text.ElideRight
+ font.weight: ethernetItem.modelData.connected ? 500 : 400
+ color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface
+ }
+
+ StyledRect {
+ implicitWidth: implicitHeight
+ implicitHeight: connectIcon.implicitHeight + Appearance.padding.small
+
+ radius: Appearance.rounding.full
+ color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0)
+
+ CircularIndicator {
+ anchors.fill: parent
+ running: ethernetItem.loading
+ }
+
+ StateLayer {
+ color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
+ disabled: ethernetItem.loading
+
+ function onClicked(): void {
+ if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) {
+ Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {});
+ } else {
+ Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {});
+ }
+ }
+ }
+
+ MaterialIcon {
+ id: connectIcon
+
+ anchors.centerIn: parent
+ animate: true
+ text: ethernetItem.modelData.connected ? "link_off" : "link"
+ color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
+
+ opacity: ethernetItem.loading ? 0 : 1
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+ }
}
}
- // Reset connecting state when network changes
Connections {
- target: Network
+ target: Nmcli
function onActiveChanged(): void {
- if (Network.active && root.connectingToSsid === Network.active.ssid) {
+ 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";
+ }
+ }
}
}
function onScanningChanged(): void {
- if (!Network.scanning)
+ if (!Nmcli.scanning)
scanIcon.rotation = 0;
}
}
+ 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/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml
new file mode 100644
index 0000000..5da50b6
--- /dev/null
+++ b/modules/bar/popouts/WirelessPassword.qml
@@ -0,0 +1,606 @@
+pragma ComponentBehavior: Bound
+
+import qs.components
+import qs.components.controls
+import qs.services
+import qs.config
+import qs.utils
+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
+ // Use Timer for actual delay to ensure dialog is fully rendered
+ focusTimer.start();
+ });
+ }
+ }
+ }
+
+ Timer {
+ id: focusTimer
+ interval: 150
+ onTriggered: {
+ root.forceActiveFocus();
+ passwordContainer.forceActiveFocus();
+ }
+ }
+
+ 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) {
+ // Use Timer for actual delay to ensure dialog is fully rendered
+ focusTimer.start();
+ }
+ }
+
+ onShouldBeVisibleChanged: {
+ if (shouldBeVisible) {
+ // Use Timer for actual delay to ensure dialog is fully rendered
+ focusTimer.start();
+ }
+ }
+
+ Keys.onEscapePressed: closeDialog()
+
+ StyledRect {
+ Layout.fillWidth: true
+ Layout.preferredWidth: 400
+ implicitHeight: content.implicitHeight + Appearance.padding.large * 2
+
+ radius: Appearance.rounding.normal
+ color: Colours.tPalette.m3surfaceContainer
+ visible: root.shouldBeVisible || root.isClosing
+ 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: root.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 || connectButton.hasError
+ text: {
+ if (connectButton.hasError) {
+ return qsTr("Connection failed. Please check your password and try again.");
+ }
+ if (connectButton.connecting) {
+ return qsTr("Connecting...");
+ }
+ return "";
+ }
+ color: connectButton.hasError ? Colours.palette.m3error : 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
+
+ property string passwordBuffer: ""
+
+ Keys.onPressed: event => {
+ // Ensure we have focus when receiving keyboard input
+ if (!activeFocus) {
+ forceActiveFocus();
+ }
+
+ // Clear error when user starts typing
+ if (connectButton.hasError && event.text && event.text.length > 0) {
+ connectButton.hasError = false;
+ }
+
+ 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;
+ }
+ }
+
+ Connections {
+ target: root
+ function onShouldBeVisibleChanged(): void {
+ if (root.shouldBeVisible) {
+ // Use Timer for actual delay to ensure focus works correctly
+ passwordFocusTimer.start();
+ passwordContainer.passwordBuffer = "";
+ connectButton.hasError = false;
+ }
+ }
+ }
+
+ Timer {
+ id: passwordFocusTimer
+ interval: 50
+ onTriggered: {
+ passwordContainer.forceActiveFocus();
+ }
+ }
+
+ Component.onCompleted: {
+ if (root.shouldBeVisible) {
+ // Use Timer for actual delay to ensure focus works correctly
+ passwordFocusTimer.start();
+ }
+ }
+
+ StyledRect {
+ anchors.fill: parent
+ radius: Appearance.rounding.normal
+ color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer
+ border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0)
+ border.color: {
+ if (connectButton.hasError) {
+ return Colours.palette.m3error;
+ }
+ if (passwordContainer.activeFocus) {
+ return Colours.palette.m3primary;
+ }
+ return root.shouldBeVisible ? Colours.palette.m3outline : "transparent";
+ }
+
+ Behavior on border.color {
+ CAnim {}
+ }
+
+ Behavior on border.width {
+ CAnim {}
+ }
+
+ Behavior on color {
+ CAnim {}
+ }
+ }
+
+ StateLayer {
+ hoverEnabled: false
+ cursorShape: Qt.IBeamCursor
+ radius: Appearance.rounding.normal
+
+ 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
+
+ TextButton {
+ id: cancelButton
+
+ Layout.fillWidth: true
+ Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
+ inactiveColour: Colours.palette.m3secondaryContainer
+ inactiveOnColour: Colours.palette.m3onSecondaryContainer
+ text: qsTr("Cancel")
+
+ onClicked: root.closeDialog()
+ }
+
+ TextButton {
+ id: connectButton
+
+ property bool connecting: false
+ property bool hasError: false
+
+ Layout.fillWidth: true
+ Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
+ inactiveColour: Colours.palette.m3primary
+ inactiveOnColour: Colours.palette.m3onPrimary
+ text: qsTr("Connect")
+ enabled: passwordContainer.passwordBuffer.length > 0 && !connecting
+
+ onClicked: {
+ if (!root.network || connecting) {
+ return;
+ }
+
+ const password = passwordContainer.passwordBuffer;
+ if (!password || password.length === 0) {
+ return;
+ }
+
+ // Clear any previous error
+ hasError = false;
+
+ // Set connecting state
+ connecting = true;
+ enabled = false;
+ text = qsTr("Connecting...");
+
+ // Connect to network
+ NetworkConnection.connectWithPassword(root.network, password, 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;
+ hasError = true;
+ enabled = true;
+ text = qsTr("Connect");
+ passwordContainer.passwordBuffer = "";
+ // Delete the failed connection
+ if (root.network && root.network.ssid) {
+ Nmcli.forgetNetwork(root.network.ssid);
+ }
+ } else {
+ // Connection failed immediately - show error
+ connectionMonitor.stop();
+ connecting = false;
+ hasError = true;
+ enabled = true;
+ text = qsTr("Connect");
+ passwordContainer.passwordBuffer = "";
+ // Delete the failed connection
+ if (root.network && root.network.ssid) {
+ Nmcli.forgetNetwork(root.network.ssid);
+ }
+ }
+ });
+
+ // 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
+ // Use Timer for actual delay
+ connectionSuccessTimer.start();
+ 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.hasError = true;
+ connectButton.enabled = true;
+ connectButton.text = qsTr("Connect");
+ passwordContainer.passwordBuffer = "";
+ // Delete the failed connection
+ if (root.network && root.network.ssid) {
+ Nmcli.forgetNetwork(root.network.ssid);
+ }
+ }
+ }
+ }
+
+ Timer {
+ id: connectionMonitor
+ interval: 1000
+ repeat: true
+ triggeredOnStart: false
+ property int repeatCount: 0
+
+ onTriggered: {
+ repeatCount++;
+ root.checkConnectionStatus();
+ }
+
+ onRunningChanged: {
+ if (!running) {
+ repeatCount = 0;
+ }
+ }
+ }
+
+ Timer {
+ id: connectionSuccessTimer
+ interval: 500
+ onTriggered: {
+ // 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();
+ }
+ }
+ }
+ }
+
+ Connections {
+ target: Nmcli
+ function onActiveChanged() {
+ if (root.shouldBeVisible) {
+ root.checkConnectionStatus();
+ }
+ }
+ function onConnectionFailed(ssid: string) {
+ if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) {
+ connectionMonitor.stop();
+ connectButton.connecting = false;
+ connectButton.hasError = true;
+ connectButton.enabled = true;
+ connectButton.text = qsTr("Connect");
+ passwordContainer.passwordBuffer = "";
+ // Delete the failed connection
+ Nmcli.forgetNetwork(ssid);
+ }
+ }
+ }
+
+ function closeDialog(): void {
+ if (isClosing) {
+ return;
+ }
+
+ isClosing = true;
+ passwordContainer.passwordBuffer = "";
+ connectButton.connecting = false;
+ connectButton.hasError = false;
+ connectButton.text = qsTr("Connect");
+ connectionMonitor.stop();
+
+ // Return to network popout
+ if (root.wrapper.currentName === "wirelesspassword") {
+ root.wrapper.currentName = "network";
+ }
+ }
+}
+
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