feat: add password overlay for network popout + redesign
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 24s
Python / fmt (pull_request) Successful in 30s
Python / lint (pull_request) Successful in 34s
Python / test (pull_request) Successful in 53s
C++ / build (pull_request) Successful in 1m52s
Rust / fmt (pull_request) Successful in 1m14s
Rust / build (pull_request) Successful in 1m57s
Rust / clippy (pull_request) Successful in 2m0s
Python / buildcheck (pull_request) Successful in 2m37s
C++ / clang-tidy (pull_request) Successful in 3m43s

This commit is contained in:
2026-07-09 17:33:13 +02:00
parent f9eeb793db
commit a64eadd14a
13 changed files with 665 additions and 123 deletions
@@ -0,0 +1,70 @@
import QtQuick
import qs.Helpers
import qs.Config
import qs.Components
CustomRect {
id: root
readonly property bool closing: PopupManager.closed
readonly property url currentPopup: PopupManager.currentPopup
property bool shouldBeVisible: loader.status === Loader.Ready
color: Qt.alpha(DynamicColors.palette.m3shadow, 0.3)
opacity: shouldBeVisible && !closing ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
onVisibleChanged: {
if (!visible) {
PopupManager.requestClose();
PopupManager.currentPopup = "";
}
}
CustomMouseArea {
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
onClicked: {
const insideItemWidth = mouseX < loader.x + loader.item.width && mouseX > loader.x;
const insideItemHeight = mouseY < loader.y + loader.item.height && mouseY > loader.y;
if (insideItemHeight && insideItemWidth)
return;
PopupManager.requestClose();
}
}
Loader {
id: loader
anchors.centerIn: parent
Connections {
function onCurrentPopupChanged(): void {
if (PopupManager.currentPopup) {
loader.setSource(PopupManager.currentPopup, PopupManager.currentPopupProps);
} else {
loader.source = null;
}
}
target: PopupManager
}
}
Connections {
function onNetworkConnected(): void {
PopupManager.requestClose();
}
target: Network
}
}
@@ -0,0 +1,102 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
import ZShell.Components
import qs.Config
import qs.Components
import qs.Helpers
CustomClippingRect {
id: root
required property var network
color: DynamicColors.palette.m3surfaceContainer
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
radius: Appearance.rounding.large
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Appearance.padding.extraLarge
spacing: Appearance.spacing.normal
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
font.pointSize: Appearance.font.size.extraLarge * 2
text: "lock"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
text: qsTr("Connect to %1").arg(root.network.name)
}
CustomTextField {
id: passwordField
Layout.fillWidth: true
Layout.preferredWidth: 400
echoMode: CustomTextField.Password
errorText: qsTr("Failed to connect with given password")
isError: Network.failedNetwork
leadingIcon: "lock"
passwordMaskDelay: 5000
placeholderText: qsTr("Input password")
}
ButtonRow {
id: buttonRow
Layout.fillWidth: true
spacing: Appearance.spacing.smaller
IconTextButton {
fillWidth: true
font.pointSize: Appearance.font.size.normal
icon: "close"
inactiveColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: DynamicColors.palette.m3onSurfaceVariant
isRound: true
isToggle: false
shapeMorph: true
text: "Cancel"
onClicked: PopupManager.requestClose()
}
IconTextButton {
fillWidth: true
font.pointSize: Appearance.font.size.normal
icon: Network.connectingNetwork ? "" : "check"
inactiveColor: Network.connectingNetwork ? DynamicColors.palette.m3primaryContainer : Network.failedNetwork ? DynamicColors.palette.m3error : DynamicColors.palette.m3primary
inactiveOnColor: Network.connectingNetwork ? DynamicColors.palette.m3onPrimaryContainer : Network.failedNetwork ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onPrimary
isRound: true
isToggle: false
shapeMorph: true
text: Network.connectingNetwork ? "" : "Apply"
onClicked: Network.requestConnect(root.network, passwordField.text)
Loader {
active: opacity > 0
anchors.centerIn: parent
opacity: Network.connectingNetwork ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: CircularIndicator {
bgColor: "transparent"
implicitSize: buttonRow.height - Appearance.padding.normal
running: true
}
}
}
}
}
}
@@ -0,0 +1,22 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
id: root
property bool closed: true
property url currentPopup: ""
property var currentPopupProps: ({})
function requestClose(): void {
closed = true;
}
function requestOpen(source: url, properties = {}): void {
currentPopupProps = properties;
currentPopup = source;
closed = false;
}
}
+222 -87
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Modules.SysTray.Popouts.Network
import qs.Components
import qs.Config
import qs.Helpers
@@ -13,7 +14,6 @@ CustomClippingRect {
required property var wrapper
anchors.horizontalCenter: parent.horizontalCenter
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: networkPopContent.height + networks.implicitHeight + networkPopContent.anchors.margins + networks.anchors.margins * 2
implicitWidth: 500 + 8 * 2
radius: (20 - Appearance.padding.small) * Appearance.rounding.scale
@@ -28,6 +28,7 @@ CustomClippingRect {
anchors.margins: Appearance.padding.large
anchors.right: parent.right
anchors.top: parent.top
spacing: Appearance.spacing.extraSmall / 2
CustomText {
Layout.preferredHeight: visible ? implicitHeight : 0
@@ -39,28 +40,17 @@ CustomClippingRect {
Toggle {
Layout.preferredHeight: visible ? implicitHeight : 0
checked: Network.wifiEnabled
label: qsTr("WiFi enabled")
first: true
last: connected.count === 0
subtext: qsTr("Toggle WiFi on or off")
text: qsTr("Set WiFi")
toggle.onToggled: Network.setWifi(checked)
}
}
ColumnLayout {
id: networks
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Appearance.padding.normal
anchors.right: parent.right
anchors.top: networkPopContent.bottom
CustomText {
Layout.leftMargin: Appearance.padding.normal
text: qsTr("Connected")
visible: Network.connectedNetworks.length > 0
onToggled: Network.setWifi(checked)
}
Repeater {
id: connected
visible: Network.connectedNetworks.length > 0
model: ScriptModel {
@@ -70,20 +60,40 @@ CustomClippingRect {
}
NetworkItem {
first: false
known: true
repeater: connected
}
}
}
ColumnLayout {
id: networks
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.right: parent.right
anchors.top: networkPopContent.bottom
spacing: Appearance.spacing.extraSmall / 2
Spacer {
repeater: known
visible: Network.connectedNetworks.length > 0
}
CustomText {
Layout.leftMargin: Appearance.padding.normal
text: qsTr("Known networks")
Layout.preferredHeight: visible ? implicitHeight : 0
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: Network.knownNetworks.length > 1 ? qsTr("%1 known networks available").arg(Network.knownNetworks.length) : qsTr("1 known network available") // qmllint disable missing-property
visible: Network.knownNetworks.length > 0
}
Repeater {
id: known
visible: Network.knownNetworks.length > 0
model: ScriptModel {
@@ -93,28 +103,27 @@ CustomClippingRect {
}
NetworkItem {
known: true
repeater: known
}
}
Spacer {
repeater: unknown
visible: Network.knownNetworks.length > 0
}
CustomText {
Layout.leftMargin: Appearance.padding.normal
text: qsTr("Networks")
}
CustomText {
Layout.leftMargin: Appearance.padding.normal
Layout.preferredHeight: visible ? implicitHeight : 0
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.extraSmall
text: qsTr("%1 networks available").arg(Network.networks.length) // qmllint disable missing-property
font.pointSize: Appearance.font.size.small
text: Network.unknownNetworks.length > 1 ? qsTr("%1 networks available").arg(Network.unknownNetworks.length) : qsTr("1 network available") // qmllint disable missing-property
visible: Network.unknownNetworks.length > 0
}
Repeater {
id: networkRepeater
id: unknown
model: ScriptModel {
values: [...Network.unknownNetworks].sort((a, b) => {
@@ -123,101 +132,227 @@ CustomClippingRect {
}
NetworkItem {
known: false
repeater: unknown
}
}
}
component NetworkItem: CustomRect {
id: knownNetworkItem
NetworkOverlay {
anchors.fill: parent
}
component ConnectedRect: CustomRect {
id: bg
property bool first
property bool last
bottomLeftRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall
bottomRightRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall
color: DynamicColors.tPalette.m3surfaceContainer
topLeftRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall
topRightRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall
}
component NetworkItem: ConnectedRect {
id: networkItem
readonly property bool connecting: Network.connectingNetwork === modelData
property int horizontalPadding: Appearance.padding.largeIncreased
required property int index
readonly property bool isSecure: Network.isSecure(modelData.security)
required property bool known
required property var modelData
required property Repeater repeater
Layout.fillWidth: true
Layout.preferredHeight: visible ? knownNetworkRow.implicitHeight + Appearance.padding.smaller * 2 : 0
Layout.rightMargin: Appearance.padding.extraSmall
Layout.preferredHeight: visible ? rowLayout.implicitHeight + Appearance.padding.smaller * 2 : 0
first: index === 0
last: index === (repeater.count - 1)
radius: Appearance.rounding.small
RowLayout {
id: knownNetworkRow
id: rowLayout
anchors.fill: parent
anchors.leftMargin: Appearance.padding.larger
anchors.rightMargin: Appearance.padding.normal
opacity: 0
scale: 0.7
spacing: Appearance.spacing.small
anchors.leftMargin: networkItem.horizontalPadding
anchors.rightMargin: networkItem.horizontalPadding
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on scale {
Anim {
}
}
Column {
id: column
Component.onCompleted: {
opacity = 1;
scale = 1;
}
MaterialIcon {
color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant
text: Icons.getNetworkIcon(knownNetworkItem.modelData.signalStrength * 100, Network.isSecure(knownNetworkItem.modelData.security))
}
CustomText {
Layout.fillWidth: true
Layout.leftMargin: Appearance.spacing.extraSmall
Layout.rightMargin: Appearance.spacing.extraSmall
color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
elide: Text.ElideRight
text: knownNetworkItem.modelData.name
CustomText {
Layout.fillWidth: true
Layout.leftMargin: Appearance.spacing.extraSmall
Layout.rightMargin: Appearance.spacing.extraSmall
color: networkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
elide: Text.ElideRight
text: networkItem.modelData.name
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: DynamicColors.palette.m3outline
elide: Text.ElideRight
font: {
const f = Qt.font(subtext.font);
f.pointSize = Appearance.font.size.small;
return f;
}
text: qsTr("%1 strength").arg((networkItem.modelData.signalStrength * 100) + "%")
}
}
CustomRect {
color: Qt.alpha(DynamicColors.palette.m3primary, knownNetworkItem.modelData.active ? 1 : 0)
implicitHeight: knownWirelessConnectIcon.implicitHeight + Appearance.padding.extraSmall
implicitWidth: implicitHeight
radius: Appearance.rounding.full
Item {
Layout.alignment: Qt.AlignRight
implicitHeight: icon.height
implicitWidth: icon.width
// CircularIndicator {
// anchors.fill: parent
// running: knownNetworkItem.loading
// }
MaterialIcon {
id: icon
color: networkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
opacity: networkItem.connecting ? 0 : 1
text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure)
Behavior on opacity {
Anim {
}
}
}
Loader {
active: opacity > 0
anchors.fill: parent
opacity: networkItem.connecting ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: CircularIndicator {
bgColor: "transparent"
running: true
}
}
}
}
StateLayer {
color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
color: networkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
onClicked: {
NetworkPassword.requestOpen();
if (networkItem.isSecure && !networkItem.known) {
PopupManager.requestOpen(Qt.resolvedUrl("Network/PSKOverlay.qml"), {
network: networkItem.modelData
});
} else {
Network.requestConnect(networkItem.modelData);
}
}
}
IconButton {
anchors.right: rowLayout.right
anchors.rightMargin: rowLayout.anchors.rightMargin + icon.width
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Appearance.font.size.large
icon: "cancel"
implicitHeight: parent.height - Appearance.padding.larger * 2
type: IconButton.Text
visible: networkItem.known
onClicked: networkItem.modelData.forget()
}
}
component Spacer: Item {
id: spacer
Layout.preferredHeight: networkRepeater.count > 0 ? Appearance.spacing.extraSmall : 0
required property Repeater repeater
Layout.preferredHeight: repeater.count > 0 ? Appearance.spacing.extraSmall : 0
}
component Toggle: RowLayout {
property alias checked: toggle.checked
required property string label
property alias toggle: toggle
component Toggle: CustomSwitch {
id: toggleItem
readonly property alias bg: bg
property alias first: bg.first
property alias last: bg.last
property string subtext
Layout.fillWidth: true
Layout.rightMargin: Appearance.padding.extraSmall
spacing: Appearance.spacing.small
cLayer: 2
horizontalPadding: Appearance.padding.largeIncreased
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
indicator.anchors.right: right
indicator.anchors.rightMargin: toggleItem.horizontalPadding
indicator.anchors.verticalCenter: verticalCenter
verticalPadding: Appearance.padding.normal
CustomText {
Layout.fillWidth: true
text: parent.label
background: ConnectedRect {
id: bg
StateLayer {
id: stateLayer
manualPressOverride: toggleItem.pressed
}
}
contentItem: Item {
anchors.left: parent.left
anchors.leftMargin: toggleItem.horizontalPadding
anchors.right: toggleItem.indicator.left
anchors.rightMargin: Appearance.spacing.normal
implicitHeight: column.implicitHeight
implicitWidth: column.implicitWidth
Column {
id: column
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
id: label
anchors.left: parent.left
anchors.right: parent.right
elide: Text.ElideRight
font: {
const f = Qt.font(label.font);
f.pointSize = Appearance.font.size.smaller;
return f;
}
text: toggleItem.text
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: DynamicColors.palette.m3outline
elide: Text.ElideRight
font: {
const f = Qt.font(subtext.font);
f.pointSize = Appearance.font.size.small;
return f;
}
text: toggleItem.subtext
visible: toggleItem.subtext
}
}
}
CustomSwitch {
id: toggle
}
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
}
}