refactor: ToggleButton Tooltips and DeviceList

This commit is contained in:
ATMDA 2025-11-19 21:04:32 -05:00
parent ff4e9bbdfd
commit ef46a02b2f
13 changed files with 914 additions and 927 deletions

View file

@ -1,5 +1,6 @@
import ".." import ".."
import qs.components import qs.components
import qs.components.controls
import qs.components.effects import qs.components.effects
import qs.services import qs.services
import qs.config import qs.config
@ -13,12 +14,31 @@ StyledRect {
property string icon property string icon
property string label property string label
property string accent: "Secondary" property string accent: "Secondary"
property real iconSize: Appearance.font.size.large
property real horizontalPadding: Appearance.padding.large
property real verticalPadding: Appearance.padding.normal
property string tooltip: ""
property bool hovered: false
signal clicked signal clicked
Component.onCompleted: {
hovered = toggleStateLayer.containsMouse;
}
Connections {
target: toggleStateLayer
function onContainsMouseChanged() {
const newHovered = toggleStateLayer.containsMouse;
if (hovered !== newHovered) {
hovered = newHovered;
}
}
}
Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0)
implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2 implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2
implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2 implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2
radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale)
color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`]
@ -46,7 +66,7 @@ StyledRect {
fill: root.toggled ? 1 : 0 fill: root.toggled ? 1 : 0
text: root.icon text: root.icon
color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`]
font.pointSize: Appearance.font.size.large font.pointSize: root.iconSize
Behavior on fill { Behavior on fill {
Anim {} Anim {}
@ -78,5 +98,30 @@ StyledRect {
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
} }
} }
// Tooltip - positioned absolutely, doesn't affect layout
Loader {
id: tooltipLoader
active: root.tooltip !== ""
asynchronous: true
z: 10000
width: 0
height: 0
sourceComponent: Component {
Tooltip {
target: root
text: root.tooltip
}
}
// Completely remove from layout
Layout.fillWidth: false
Layout.fillHeight: false
Layout.preferredWidth: 0
Layout.preferredHeight: 0
Layout.maximumWidth: 0
Layout.maximumHeight: 0
Layout.minimumWidth: 0
Layout.minimumHeight: 0
}
} }

View file

@ -0,0 +1,170 @@
import ".."
import qs.components.effects
import qs.services
import qs.config
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Popup {
id: root
required property Item target
required property string text
property int delay: 500
property int timeout: 0
property bool tooltipVisible: false
property Timer showTimer: Timer {
interval: root.delay
onTriggered: root.tooltipVisible = true
}
property Timer hideTimer: Timer {
interval: root.timeout
onTriggered: root.tooltipVisible = false
}
// Popup properties - doesn't affect layout
parent: {
let p = target;
// Walk up to find the root Item (usually has anchors.fill: parent)
while (p && p.parent) {
const parentItem = p.parent;
// Check if this looks like a root pane Item
if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) {
return parentItem;
}
p = parentItem;
}
// Fallback
return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target;
}
visible: tooltipVisible
modal: false
closePolicy: Popup.NoAutoClose
padding: 0
margins: 0
// Update position when target moves or tooltip becomes visible
onTooltipVisibleChanged: {
if (tooltipVisible) {
Qt.callLater(updatePosition);
}
}
Connections {
target: root.target
function onXChanged() { if (root.tooltipVisible) root.updatePosition(); }
function onYChanged() { if (root.tooltipVisible) root.updatePosition(); }
function onWidthChanged() { if (root.tooltipVisible) root.updatePosition(); }
function onHeightChanged() { if (root.tooltipVisible) root.updatePosition(); }
}
function updatePosition() {
if (!target || !parent) return;
// Wait for tooltipRect to have its size calculated
Qt.callLater(() => {
if (!target || !parent || !tooltipRect) return;
// Get target position in parent's coordinate system
const targetPos = target.mapToItem(parent, 0, 0);
const targetCenterX = targetPos.x + target.width / 2;
// Get tooltip size (use width/height if available, otherwise implicit)
const tooltipWidth = tooltipRect.width > 0 ? tooltipRect.width : tooltipRect.implicitWidth;
const tooltipHeight = tooltipRect.height > 0 ? tooltipRect.height : tooltipRect.implicitHeight;
// Center tooltip horizontally on target
let newX = targetCenterX - tooltipWidth / 2;
// Position tooltip above target
let newY = targetPos.y - tooltipHeight - Appearance.spacing.small;
// Keep within bounds
const padding = Appearance.padding.normal;
if (newX < padding) {
newX = padding;
} else if (newX + tooltipWidth > (parent.width - padding)) {
newX = parent.width - tooltipWidth - padding;
}
// Update popup position
x = newX;
y = newY;
});
}
enter: Transition {
Anim {
property: "opacity"
from: 0
to: 1
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
exit: Transition {
Anim {
property: "opacity"
from: 1
to: 0
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
// Monitor hover state
Connections {
target: root.target
function onHoveredChanged() {
if (target.hovered) {
showTimer.start();
if (timeout > 0) {
hideTimer.stop();
hideTimer.start();
}
} else {
showTimer.stop();
hideTimer.stop();
tooltipVisible = false;
}
}
}
contentItem: StyledRect {
id: tooltipRect
implicitWidth: tooltipText.implicitWidth + Appearance.padding.normal * 2
implicitHeight: tooltipText.implicitHeight + Appearance.padding.smaller * 2
color: Colours.palette.m3surfaceContainerHighest
radius: Appearance.rounding.small
// Add elevation for depth
Elevation {
anchors.fill: parent
radius: parent.radius
z: -1
level: 3
}
StyledText {
id: tooltipText
anchors.centerIn: parent
text: root.text
color: Colours.palette.m3onSurface
font.pointSize: Appearance.font.size.small
}
}
Component.onCompleted: {
if (tooltipVisible) {
updatePosition();
}
}
}

View file

@ -24,8 +24,23 @@ SplitPaneWithDetails {
} }
leftContent: Component { leftContent: Component {
DeviceList { StyledFlickable {
session: root.session id: leftFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: deviceList.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: leftFlickable
}
DeviceList {
id: deviceList
anchors.left: parent.left
anchors.right: parent.right
session: root.session
}
} }
} }

View file

@ -20,404 +20,427 @@ StyledFlickable {
readonly property BluetoothDevice device: session.bt.active readonly property BluetoothDevice device: session.bt.active
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
contentHeight: layoutWrapper.height contentHeight: detailsWrapper.height
StyledScrollBar.vertical: StyledScrollBar { StyledScrollBar.vertical: StyledScrollBar {
flickable: root flickable: root
} }
Item { Item {
id: layoutWrapper id: detailsWrapper
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
implicitHeight: details.implicitHeight
DeviceDetails {
id: details
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
implicitHeight: layout.height
ColumnLayout { session: root.session
id: layout device: root.device
anchors.left: parent.left headerComponent: Component {
anchors.right: parent.right SettingsHeader {
anchors.top: parent.top icon: Icons.getBluetoothIcon(root.device?.icon ?? "")
spacing: Appearance.spacing.normal title: root.device?.name ?? ""
SettingsHeader {
icon: Icons.getBluetoothIcon(root.device?.icon ?? "")
title: root.device?.name ?? ""
}
StyledText {
Layout.topMargin: Appearance.spacing.large
text: qsTr("Connection status")
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
StyledText {
text: qsTr("Connection settings for this device")
color: Colours.palette.m3outline
}
StyledRect {
Layout.fillWidth: true
implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
id: deviceStatus
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.larger
Toggle {
label: qsTr("Connected")
checked: root.device?.connected ?? false
toggle.onToggled: root.device.connected = checked
}
Toggle {
label: qsTr("Paired")
checked: root.device?.paired ?? false
toggle.onToggled: {
if (root.device.paired)
root.device.forget();
else
root.device.pair();
}
}
Toggle {
label: qsTr("Blocked")
checked: root.device?.blocked ?? false
toggle.onToggled: root.device.blocked = checked
}
} }
} }
StyledText { sections: [
Layout.topMargin: Appearance.spacing.large Component {
text: qsTr("Device properties") ColumnLayout {
font.pointSize: Appearance.font.size.larger spacing: Appearance.spacing.normal
font.weight: 500
}
StyledText { StyledText {
text: qsTr("Additional settings") Layout.topMargin: Appearance.spacing.large
color: Colours.palette.m3outline text: qsTr("Connection status")
} font.pointSize: Appearance.font.size.larger
font.weight: 500
StyledRect {
Layout.fillWidth: true
implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
id: deviceProps
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.larger
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
Item {
id: renameDevice
Layout.fillWidth: true
Layout.rightMargin: Appearance.spacing.small
implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight
states: State {
name: "editingDeviceName"
when: root.session.bt.editingDeviceName
AnchorChanges {
target: deviceNameEdit
anchors.top: renameDevice.top
}
PropertyChanges {
renameDevice.implicitHeight: deviceNameEdit.implicitHeight
renameLabel.opacity: 0
deviceNameEdit.padding: Appearance.padding.normal
}
}
transitions: Transition {
AnchorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
Anim {
properties: "implicitHeight,opacity,padding"
}
}
StyledText {
id: renameLabel
anchors.left: parent.left
text: qsTr("Device name")
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
StyledTextField {
id: deviceNameEdit
anchors.left: parent.left
anchors.right: parent.right
anchors.top: renameLabel.bottom
anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal
text: root.device?.name ?? ""
readOnly: !root.session.bt.editingDeviceName
onAccepted: {
root.session.bt.editingDeviceName = false;
root.device.name = text;
}
leftPadding: Appearance.padding.normal
rightPadding: Appearance.padding.normal
background: StyledRect {
radius: Appearance.rounding.small
border.width: 2
border.color: Colours.palette.m3primary
opacity: root.session.bt.editingDeviceName ? 1 : 0
Behavior on border.color {
CAnim {}
}
Behavior on opacity {
Anim {}
}
}
Behavior on anchors.leftMargin {
Anim {}
}
}
} }
StyledRect { StyledText {
implicitWidth: implicitHeight text: qsTr("Connection settings for this device")
implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 color: Colours.palette.m3outline
radius: Appearance.rounding.small
color: Colours.palette.m3secondaryContainer
opacity: root.session.bt.editingDeviceName ? 1 : 0
scale: root.session.bt.editingDeviceName ? 1 : 0.5
StateLayer {
color: Colours.palette.m3onSecondaryContainer
disabled: !root.session.bt.editingDeviceName
function onClicked(): void {
root.session.bt.editingDeviceName = false;
deviceNameEdit.text = Qt.binding(() => root.device?.name ?? "");
}
}
MaterialIcon {
id: cancelEditIcon
anchors.centerIn: parent
animate: true
text: "cancel"
color: Colours.palette.m3onSecondaryContainer
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2
radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0)
StateLayer {
color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
function onClicked(): void {
root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName;
if (root.session.bt.editingDeviceName)
deviceNameEdit.forceActiveFocus();
else
deviceNameEdit.accepted();
}
}
MaterialIcon {
id: editIcon
anchors.centerIn: parent
animate: true
text: root.session.bt.editingDeviceName ? "check_circle" : "edit"
color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
}
Behavior on radius {
Anim {}
}
}
}
Toggle {
label: qsTr("Trusted")
checked: root.device?.trusted ?? false
toggle.onToggled: root.device.trusted = checked
}
Toggle {
label: qsTr("Wake allowed")
checked: root.device?.wakeAllowed ?? false
toggle.onToggled: root.device.wakeAllowed = checked
}
}
}
StyledText {
Layout.topMargin: Appearance.spacing.large
text: qsTr("Device information")
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
StyledText {
text: qsTr("Information about this device")
color: Colours.palette.m3outline
}
StyledRect {
Layout.fillWidth: true
implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
id: deviceInfo
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small / 2
StyledText {
text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable")
}
RowLayout {
Layout.topMargin: Appearance.spacing.small / 2
Layout.fillWidth: true
Layout.preferredHeight: Appearance.padding.smaller
spacing: Appearance.spacing.small / 2
StyledRect {
Layout.fillHeight: true
implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0
radius: Appearance.rounding.full
color: Colours.palette.m3primary
} }
StyledRect { StyledRect {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2
radius: Appearance.rounding.full
color: Colours.palette.m3secondaryContainer
StyledRect { radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
id: deviceStatus
anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.verticalCenter: parent.verticalCenter
anchors.bottom: parent.bottom anchors.margins: Appearance.padding.large
anchors.margins: parent.height * 0.25
implicitWidth: height spacing: Appearance.spacing.larger
radius: Appearance.rounding.full
color: Colours.palette.m3primary Toggle {
label: qsTr("Connected")
checked: root.device?.connected ?? false
toggle.onToggled: root.device.connected = checked
}
Toggle {
label: qsTr("Paired")
checked: root.device?.paired ?? false
toggle.onToggled: {
if (root.device.paired)
root.device.forget();
else
root.device.pair();
}
}
Toggle {
label: qsTr("Blocked")
checked: root.device?.blocked ?? false
toggle.onToggled: root.device.blocked = checked
}
} }
} }
} }
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
StyledText { StyledText {
Layout.topMargin: Appearance.spacing.normal Layout.topMargin: Appearance.spacing.large
text: qsTr("Dbus path") text: qsTr("Device properties")
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
StyledText {
text: qsTr("Additional settings")
color: Colours.palette.m3outline
}
StyledRect {
Layout.fillWidth: true
implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
id: deviceProps
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.larger
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
Item {
id: renameDevice
Layout.fillWidth: true
Layout.rightMargin: Appearance.spacing.small
implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight
states: State {
name: "editingDeviceName"
when: root.session.bt.editingDeviceName
AnchorChanges {
target: deviceNameEdit
anchors.top: renameDevice.top
}
PropertyChanges {
renameDevice.implicitHeight: deviceNameEdit.implicitHeight
renameLabel.opacity: 0
deviceNameEdit.padding: Appearance.padding.normal
}
}
transitions: Transition {
AnchorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
Anim {
properties: "implicitHeight,opacity,padding"
}
}
StyledText {
id: renameLabel
anchors.left: parent.left
text: qsTr("Device name")
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
StyledTextField {
id: deviceNameEdit
anchors.left: parent.left
anchors.right: parent.right
anchors.top: renameLabel.bottom
anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal
text: root.device?.name ?? ""
readOnly: !root.session.bt.editingDeviceName
onAccepted: {
root.session.bt.editingDeviceName = false;
root.device.name = text;
}
leftPadding: Appearance.padding.normal
rightPadding: Appearance.padding.normal
background: StyledRect {
radius: Appearance.rounding.small
border.width: 2
border.color: Colours.palette.m3primary
opacity: root.session.bt.editingDeviceName ? 1 : 0
Behavior on border.color {
CAnim {}
}
Behavior on opacity {
Anim {}
}
}
Behavior on anchors.leftMargin {
Anim {}
}
}
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2
radius: Appearance.rounding.small
color: Colours.palette.m3secondaryContainer
opacity: root.session.bt.editingDeviceName ? 1 : 0
scale: root.session.bt.editingDeviceName ? 1 : 0.5
StateLayer {
color: Colours.palette.m3onSecondaryContainer
disabled: !root.session.bt.editingDeviceName
function onClicked(): void {
root.session.bt.editingDeviceName = false;
deviceNameEdit.text = Qt.binding(() => root.device?.name ?? "");
}
}
MaterialIcon {
id: cancelEditIcon
anchors.centerIn: parent
animate: true
text: "cancel"
color: Colours.palette.m3onSecondaryContainer
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2
radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0)
StateLayer {
color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
function onClicked(): void {
root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName;
if (root.session.bt.editingDeviceName)
deviceNameEdit.forceActiveFocus();
else
deviceNameEdit.accepted();
}
}
MaterialIcon {
id: editIcon
anchors.centerIn: parent
animate: true
text: root.session.bt.editingDeviceName ? "check_circle" : "edit"
color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
}
Behavior on radius {
Anim {}
}
}
}
Toggle {
label: qsTr("Trusted")
checked: root.device?.trusted ?? false
toggle.onToggled: root.device.trusted = checked
}
Toggle {
label: qsTr("Wake allowed")
checked: root.device?.wakeAllowed ?? false
toggle.onToggled: root.device.wakeAllowed = checked
}
}
}
} }
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
StyledText { StyledText {
text: root.device?.dbusPath ?? "" Layout.topMargin: Appearance.spacing.large
color: Colours.palette.m3outline text: qsTr("Device information")
font.pointSize: Appearance.font.size.small font.pointSize: Appearance.font.size.larger
} font.weight: 500
}
StyledText { StyledText {
Layout.topMargin: Appearance.spacing.normal text: qsTr("Information about this device")
text: qsTr("MAC address") color: Colours.palette.m3outline
} }
StyledText { StyledRect {
text: root.device?.address ?? "" Layout.fillWidth: true
color: Colours.palette.m3outline implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2
font.pointSize: Appearance.font.size.small
}
StyledText { radius: Appearance.rounding.normal
Layout.topMargin: Appearance.spacing.normal color: Colours.tPalette.m3surfaceContainer
text: qsTr("Bonded")
}
StyledText { ColumnLayout {
text: root.device?.bonded ? qsTr("Yes") : qsTr("No") id: deviceInfo
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
StyledText { anchors.left: parent.left
Layout.topMargin: Appearance.spacing.normal anchors.right: parent.right
text: qsTr("System name") anchors.verticalCenter: parent.verticalCenter
} anchors.margins: Appearance.padding.large
StyledText { spacing: Appearance.spacing.small / 2
text: root.device?.deviceName ?? ""
color: Colours.palette.m3outline StyledText {
font.pointSize: Appearance.font.size.small text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable")
}
RowLayout {
Layout.topMargin: Appearance.spacing.small / 2
Layout.fillWidth: true
Layout.preferredHeight: Appearance.padding.smaller
spacing: Appearance.spacing.small / 2
StyledRect {
Layout.fillHeight: true
implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0
radius: Appearance.rounding.full
color: Colours.palette.m3primary
}
StyledRect {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Appearance.rounding.full
color: Colours.palette.m3secondaryContainer
StyledRect {
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: parent.height * 0.25
implicitWidth: height
radius: Appearance.rounding.full
color: Colours.palette.m3primary
}
}
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("Dbus path")
}
StyledText {
text: root.device?.dbusPath ?? ""
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("MAC address")
}
StyledText {
text: root.device?.address ?? ""
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("Bonded")
}
StyledText {
text: root.device?.bonded ? qsTr("Yes") : qsTr("No")
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("System name")
}
StyledText {
text: root.device?.deviceName ?? ""
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
}
}
} }
} }
} ]
}
} }
}
// FAB Menu (positioned absolutely relative to flickable)
ColumnLayout { ColumnLayout {
anchors.right: fabRoot.right anchors.right: fabRoot.right
anchors.bottom: fabRoot.top anchors.bottom: fabRoot.top

View file

@ -48,8 +48,12 @@ DeviceList {
toggled: Bluetooth.defaultAdapter?.enabled ?? false toggled: Bluetooth.defaultAdapter?.enabled ?? false
icon: "power" icon: "power"
accent: "Tertiary" accent: "Tertiary"
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
tooltip: qsTr("Toggle Bluetooth")
function onClicked(): void { onClicked: {
const adapter = Bluetooth.defaultAdapter; const adapter = Bluetooth.defaultAdapter;
if (adapter) if (adapter)
adapter.enabled = !adapter.enabled; adapter.enabled = !adapter.enabled;
@ -60,8 +64,12 @@ DeviceList {
toggled: Bluetooth.defaultAdapter?.discoverable ?? false toggled: Bluetooth.defaultAdapter?.discoverable ?? false
icon: root.smallDiscoverable ? "group_search" : "" icon: root.smallDiscoverable ? "group_search" : ""
label: root.smallDiscoverable ? "" : qsTr("Discoverable") label: root.smallDiscoverable ? "" : qsTr("Discoverable")
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
tooltip: qsTr("Make discoverable")
function onClicked(): void { onClicked: {
const adapter = Bluetooth.defaultAdapter; const adapter = Bluetooth.defaultAdapter;
if (adapter) if (adapter)
adapter.discoverable = !adapter.discoverable; adapter.discoverable = !adapter.discoverable;
@ -72,20 +80,44 @@ DeviceList {
toggled: Bluetooth.defaultAdapter?.pairable ?? false toggled: Bluetooth.defaultAdapter?.pairable ?? false
icon: "missing_controller" icon: "missing_controller"
label: root.smallPairable ? "" : qsTr("Pairable") label: root.smallPairable ? "" : qsTr("Pairable")
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
tooltip: qsTr("Make pairable")
function onClicked(): void { onClicked: {
const adapter = Bluetooth.defaultAdapter; const adapter = Bluetooth.defaultAdapter;
if (adapter) if (adapter)
adapter.pairable = !adapter.pairable; adapter.pairable = !adapter.pairable;
} }
} }
ToggleButton {
toggled: Bluetooth.defaultAdapter?.discovering ?? false
icon: "bluetooth_searching"
accent: "Secondary"
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
tooltip: qsTr("Scan for devices")
onClicked: {
const adapter = Bluetooth.defaultAdapter;
if (adapter)
adapter.discovering = !adapter.discovering;
}
}
ToggleButton { ToggleButton {
toggled: !root.session.bt.active toggled: !root.session.bt.active
icon: "settings" icon: "settings"
accent: "Primary" accent: "Primary"
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
tooltip: qsTr("Bluetooth settings")
function onClicked(): void { onClicked: {
if (root.session.bt.active) if (root.session.bt.active)
root.session.bt.active = null; root.session.bt.active = null;
else { else {
@ -96,47 +128,6 @@ DeviceList {
} }
} }
titleSuffix: Component {
RowLayout {
spacing: Appearance.spacing.normal
Item {
Layout.fillWidth: true
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: scanIcon.implicitHeight + Appearance.padding.normal * 2
radius: Bluetooth.defaultAdapter?.discovering ? Appearance.rounding.normal : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
StateLayer {
color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
function onClicked(): void {
const adapter = Bluetooth.defaultAdapter;
if (adapter)
adapter.discovering = !adapter.discovering;
}
}
MaterialIcon {
id: scanIcon
anchors.centerIn: parent
animate: true
text: "bluetooth_searching"
color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
fill: Bluetooth.defaultAdapter?.discovering ? 1 : 0
}
Behavior on radius {
Anim {}
}
}
}
}
delegate: Component { delegate: Component {
StyledRect { StyledRect {
@ -146,8 +137,7 @@ DeviceList {
readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting) readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting)
readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected
anchors.left: parent.left width: ListView.view ? ListView.view.width : undefined
anchors.right: parent.right
implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2 implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)

View file

@ -90,6 +90,8 @@ Item {
model: root.sections model: root.sections
Loader { Loader {
required property Component modelData
Layout.fillWidth: true Layout.fillWidth: true
sourceComponent: modelData sourceComponent: modelData
} }

View file

@ -6,6 +6,7 @@ import qs.components.controls
import qs.components.containers import qs.components.containers
import qs.services import qs.services
import qs.config import qs.config
import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
@ -55,6 +56,7 @@ ColumnLayout {
property var activeItem: null property var activeItem: null
property Component headerComponent: null property Component headerComponent: null
property Component titleSuffix: null property Component titleSuffix: null
property bool showHeader: true
signal itemSelected(var item) signal itemSelected(var item)
@ -66,7 +68,7 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
sourceComponent: root.headerComponent sourceComponent: root.headerComponent
visible: root.headerComponent !== null visible: root.headerComponent !== null && root.showHeader
} }
// Title and description row // Title and description row
@ -109,17 +111,15 @@ ColumnLayout {
id: view id: view
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true // Use contentHeight to show all items without estimation
implicitHeight: contentHeight
model: root.model model: root.model
delegate: root.delegate delegate: root.delegate
spacing: Appearance.spacing.small / 2 spacing: Appearance.spacing.small / 2
clip: true interactive: false // Disable individual scrolling - parent pane handles it
clip: false // Don't clip - let parent handle scrolling
StyledScrollBar.vertical: StyledScrollBar {
flickable: view
}
} }
} }

View file

@ -183,6 +183,10 @@ Item {
toggled: !root.session.launcher.active toggled: !root.session.launcher.active
icon: "settings" icon: "settings"
accent: "Primary" accent: "Primary"
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
tooltip: qsTr("Launcher settings")
onClicked: { onClicked: {
if (root.session.launcher.active) { if (root.session.launcher.active) {

View file

@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import ".." import ".."
import "../components"
import qs.components import qs.components
import qs.components.controls import qs.components.controls
import qs.components.effects import qs.components.effects
@ -10,99 +11,108 @@ import qs.config
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
Item { DeviceDetails {
id: root id: root
required property Session session required property Session session
readonly property var device: session.ethernet.active readonly property var ethernetDevice: session.ethernet.active
implicitWidth: layout.implicitWidth device: ethernetDevice
implicitHeight: layout.implicitHeight
Component.onCompleted: { Component.onCompleted: {
if (device && device.interface) { if (ethernetDevice && ethernetDevice.interface) {
Nmcli.getEthernetDeviceDetails(device.interface, () => {}); Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {});
} }
} }
onDeviceChanged: { onEthernetDeviceChanged: {
if (device && device.interface) { if (ethernetDevice && ethernetDevice.interface) {
Nmcli.getEthernetDeviceDetails(device.interface, () => {}); Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {});
} else { } else {
Nmcli.ethernetDeviceDetails = null; Nmcli.ethernetDeviceDetails = null;
} }
} }
ColumnLayout { headerComponent: Component {
id: layout ConnectionHeader {
icon: "cable"
title: root.ethernetDevice?.interface ?? qsTr("Unknown")
}
}
anchors.left: parent.left sections: [
anchors.right: parent.right Component {
anchors.top: parent.top ColumnLayout {
spacing: Appearance.spacing.normal spacing: Appearance.spacing.normal
ConnectionHeader { SectionHeader {
icon: "cable" title: qsTr("Connection status")
title: root.device?.interface ?? qsTr("Unknown") description: qsTr("Connection settings for this device")
} }
SectionHeader { SectionContainer {
title: qsTr("Connection status") ToggleRow {
description: qsTr("Connection settings for this device") label: qsTr("Connected")
} checked: root.ethernetDevice?.connected ?? false
toggle.onToggled: {
SectionContainer { if (checked) {
ToggleRow { Nmcli.connectEthernet(root.ethernetDevice?.connection || "", root.ethernetDevice?.interface || "", () => {});
label: qsTr("Connected") } else {
checked: root.device?.connected ?? false if (root.ethernetDevice?.connection) {
toggle.onToggled: { Nmcli.disconnectEthernet(root.ethernetDevice.connection, () => {});
if (checked) { }
Nmcli.connectEthernet(root.device?.connection || "", root.device?.interface || "", () => {});
} else {
if (root.device?.connection) {
Nmcli.disconnectEthernet(root.device.connection, () => {});
} }
} }
} }
} }
} }
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader { SectionHeader {
title: qsTr("Device properties") title: qsTr("Device properties")
description: qsTr("Additional information") description: qsTr("Additional information")
}
SectionContainer {
contentSpacing: Appearance.spacing.small / 2
PropertyRow {
label: qsTr("Interface")
value: root.device?.interface ?? qsTr("Unknown")
} }
PropertyRow { SectionContainer {
showTopMargin: true contentSpacing: Appearance.spacing.small / 2
label: qsTr("Connection")
value: root.device?.connection || qsTr("Not connected")
}
PropertyRow { PropertyRow {
showTopMargin: true label: qsTr("Interface")
label: qsTr("State") value: root.ethernetDevice?.interface ?? qsTr("Unknown")
value: root.device?.state ?? qsTr("Unknown") }
PropertyRow {
showTopMargin: true
label: qsTr("Connection")
value: root.ethernetDevice?.connection || qsTr("Not connected")
}
PropertyRow {
showTopMargin: true
label: qsTr("State")
value: root.ethernetDevice?.state ?? qsTr("Unknown")
}
} }
} }
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader { SectionHeader {
title: qsTr("Connection information") title: qsTr("Connection information")
description: qsTr("Network connection details") description: qsTr("Network connection details")
} }
SectionContainer { SectionContainer {
ConnectionInfoSection { ConnectionInfoSection {
deviceDetails: Nmcli.ethernetDeviceDetails deviceDetails: Nmcli.ethernetDeviceDetails
}
} }
} }
} }
]
} }

View file

@ -39,6 +39,9 @@ DeviceList {
toggled: !root.session.ethernet.active toggled: !root.session.ethernet.active
icon: "settings" icon: "settings"
accent: "Primary" accent: "Primary"
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
onClicked: { onClicked: {
if (root.session.ethernet.active) if (root.session.ethernet.active)
@ -55,8 +58,7 @@ DeviceList {
StyledRect { StyledRect {
required property var modelData required property var modelData
anchors.left: parent.left width: ListView.view ? ListView.view.width : undefined
anchors.right: parent.right
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal radius: Appearance.rounding.normal

View file

@ -28,23 +28,22 @@ Item {
anchors.fill: parent anchors.fill: parent
leftContent: Component { leftContent: Component {
StyledFlickable {
id: leftFlickable
StyledFlickable { flickableDirection: Flickable.VerticalFlick
id: leftFlickable contentHeight: leftContent.height
flickableDirection: Flickable.VerticalFlick StyledScrollBar.vertical: StyledScrollBar {
contentHeight: leftContent.height flickable: leftFlickable
}
StyledScrollBar.vertical: StyledScrollBar { ColumnLayout {
flickable: leftFlickable id: leftContent
}
ColumnLayout { anchors.left: parent.left
id: leftContent anchors.right: parent.right
spacing: Appearance.spacing.normal
anchors.left: parent.left
anchors.right: parent.right
spacing: Appearance.spacing.normal
// Network header above the collapsible sections // Network header above the collapsible sections
RowLayout { RowLayout {
@ -65,6 +64,10 @@ Item {
toggled: Nmcli.wifiEnabled toggled: Nmcli.wifiEnabled
icon: "wifi" icon: "wifi"
accent: "Tertiary" accent: "Tertiary"
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
tooltip: qsTr("Toggle WiFi")
onClicked: { onClicked: {
Nmcli.toggleWifi(null); Nmcli.toggleWifi(null);
@ -75,6 +78,10 @@ Item {
toggled: Nmcli.scanning toggled: Nmcli.scanning
icon: "wifi_find" icon: "wifi_find"
accent: "Secondary" accent: "Secondary"
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
tooltip: qsTr("Scan for networks")
onClicked: { onClicked: {
Nmcli.rescanWifi(); Nmcli.rescanWifi();
@ -85,6 +92,10 @@ Item {
toggled: !root.session.ethernet.active && !root.session.network.active toggled: !root.session.ethernet.active && !root.session.network.active
icon: "settings" icon: "settings"
accent: "Primary" accent: "Primary"
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
tooltip: qsTr("Network settings")
onClicked: { onClicked: {
if (root.session.ethernet.active || root.session.network.active) { if (root.session.ethernet.active || root.session.network.active) {
@ -109,127 +120,12 @@ Item {
title: qsTr("Ethernet") title: qsTr("Ethernet")
expanded: true expanded: true
ColumnLayout { Loader {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Appearance.spacing.small sourceComponent: Component {
EthernetList {
RowLayout { session: root.session
Layout.fillWidth: true showHeader: false
spacing: Appearance.spacing.small
StyledText {
text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length)
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
}
StyledText {
Layout.fillWidth: true
text: qsTr("All available ethernet devices")
color: Colours.palette.m3outline
}
Repeater {
id: ethernetRepeater
Layout.fillWidth: true
model: Nmcli.ethernetDevices
delegate: StyledRect {
required property var modelData
Layout.fillWidth: true
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
StateLayer {
function onClicked(): void {
root.session.network.active = null;
root.session.ethernet.active = modelData;
}
}
RowLayout {
id: rowLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledRect {
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
radius: Appearance.rounding.normal
color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
MaterialIcon {
id: icon
anchors.centerIn: parent
text: "cable"
font.pointSize: Appearance.font.size.large
fill: modelData.connected ? 1 : 0
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
maximumLineCount: 1
text: modelData.interface || qsTr("Unknown")
}
StyledText {
Layout.fillWidth: true
text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected")
color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
font.weight: modelData.connected ? 500 : 400
elide: Text.ElideRight
}
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
radius: Appearance.rounding.full
color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0)
StateLayer {
function onClicked(): void {
if (modelData.connected && modelData.connection) {
Nmcli.disconnectEthernet(modelData.connection, () => {});
} else {
Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {});
}
}
}
MaterialIcon {
id: connectIcon
anchors.centerIn: parent
text: modelData.connected ? "link_off" : "link"
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
}
}
}
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
} }
} }
} }
@ -242,195 +138,12 @@ Item {
title: qsTr("Wireless") title: qsTr("Wireless")
expanded: true expanded: true
ColumnLayout { Loader {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Appearance.spacing.small sourceComponent: Component {
WirelessList {
RowLayout { session: root.session
Layout.fillWidth: true showHeader: false
spacing: Appearance.spacing.small
StyledText {
text: qsTr("Networks (%1)").arg(Nmcli.networks.length)
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
StyledText {
visible: Nmcli.scanning
text: qsTr("Scanning...")
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.small
}
}
StyledText {
Layout.fillWidth: true
text: qsTr("All available WiFi networks")
color: Colours.palette.m3outline
}
Repeater {
id: wirelessRepeater
Layout.fillWidth: true
model: ScriptModel {
values: [...Nmcli.networks].sort((a, b) => {
// Put active/connected network first
if (a.active !== b.active)
return b.active - a.active;
// Then sort by signal strength
return b.strength - a.strength;
})
}
delegate: StyledRect {
required property var modelData
Layout.fillWidth: true
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (modelData && root.session.network.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
StateLayer {
function onClicked(): void {
if (!modelData) {
return;
}
root.session.ethernet.active = null;
root.session.network.active = modelData;
// Check if we need to refresh saved connections when selecting a network
if (modelData.ssid) {
checkSavedProfileForNetwork(modelData.ssid);
}
}
}
RowLayout {
id: wirelessRowLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledRect {
implicitWidth: implicitHeight
implicitHeight: wirelessIcon.implicitHeight + Appearance.padding.normal * 2
radius: Appearance.rounding.normal
color: (modelData && modelData.active) ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
MaterialIcon {
id: wirelessIcon
anchors.centerIn: parent
text: Icons.getNetworkIcon(modelData && modelData.strength !== undefined ? modelData.strength : 0)
font.pointSize: Appearance.font.size.large
fill: (modelData && modelData.active) ? 1 : 0
color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
}
StyledRect {
id: lockBadge
visible: modelData && modelData.isSecure
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: -Appearance.padding.smaller / 2
implicitWidth: lockIconSize + Appearance.padding.smaller
implicitHeight: lockIconSize + Appearance.padding.smaller
radius: Appearance.rounding.full
color: Colours.palette.m3secondaryContainer
readonly property real lockIconSize: lockIcon.implicitWidth
Elevation {
anchors.fill: parent
radius: parent.radius
z: -1
level: 2
}
MaterialIcon {
id: lockIcon
anchors.centerIn: parent
text: "lock"
font.pointSize: Appearance.font.size.small
fill: 1
color: Colours.palette.m3onSurface
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
maximumLineCount: 1
text: (modelData && modelData.ssid) ? modelData.ssid : qsTr("Unknown")
}
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.smaller
StyledText {
Layout.fillWidth: true
text: {
if (!modelData) return qsTr("Open");
if (modelData.active) return qsTr("Connected");
if (modelData.isSecure && modelData.security && modelData.security.length > 0) {
return modelData.security;
}
if (modelData.isSecure) return qsTr("Secured");
return qsTr("Open");
}
color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
font.weight: (modelData && modelData.active) ? 500 : 400
elide: Text.ElideRight
}
}
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2
radius: Appearance.rounding.full
color: Qt.alpha(Colours.palette.m3primaryContainer, (modelData && modelData.active) ? 1 : 0)
StateLayer {
function onClicked(): void {
if (modelData && modelData.active) {
Nmcli.disconnectFromNetwork();
} else if (modelData) {
NetworkConnection.handleConnect(modelData, root.session, null);
}
}
}
MaterialIcon {
id: wirelessConnectIcon
anchors.centerIn: parent
text: (modelData && modelData.active) ? "link_off" : "link"
color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
}
}
}
implicitHeight: wirelessRowLayout.implicitHeight + Appearance.padding.normal * 2
} }
} }
} }
@ -443,16 +156,17 @@ Item {
Item { Item {
id: rightPaneItem id: rightPaneItem
// Right pane - networking details/settings
property var ethernetPane: root.session.ethernet.active property var ethernetPane: root.session.ethernet.active
property var wirelessPane: root.session.network.active property var wirelessPane: root.session.network.active
property var pane: ethernetPane || wirelessPane property var pane: ethernetPane || wirelessPane
property string paneId: ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings") property string paneId: ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings")
property Component targetComponent: settings property Component targetComponent: settingsComponent
property Component nextComponent: settings property Component nextComponent: settingsComponent
function getComponentForPane() { function getComponentForPane() {
return pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings; if (ethernetPane) return ethernetDetailsComponent;
if (wirelessPane) return wirelessDetailsComponent;
return settingsComponent;
} }
Component.onCompleted: { Component.onCompleted: {
@ -507,7 +221,7 @@ Item {
} }
Component { Component {
id: settings id: settingsComponent
StyledFlickable { StyledFlickable {
id: settingsFlickable id: settingsFlickable
@ -530,7 +244,7 @@ Item {
} }
Component { Component {
id: ethernetDetails id: ethernetDetailsComponent
StyledFlickable { StyledFlickable {
id: ethernetFlickable id: ethernetFlickable
@ -553,7 +267,7 @@ Item {
} }
Component { Component {
id: wirelessDetails id: wirelessDetailsComponent
StyledFlickable { StyledFlickable {
id: wirelessFlickable id: wirelessFlickable
@ -580,11 +294,4 @@ Item {
session: root.session session: root.session
z: 1000 z: 1000
} }
function checkSavedProfileForNetwork(ssid: string): void {
if (ssid && ssid.length > 0) {
Nmcli.loadSavedConnections(() => {});
}
}
} }

View file

@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import ".." import ".."
import "../components"
import "." import "."
import qs.components import qs.components
import qs.components.controls import qs.components.controls
@ -12,14 +13,13 @@ import qs.utils
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
Item { DeviceDetails {
id: root id: root
required property Session session required property Session session
readonly property var network: session.network.active readonly property var network: session.network.active
implicitWidth: layout.implicitWidth device: network
implicitHeight: layout.implicitHeight
Component.onCompleted: { Component.onCompleted: {
updateDeviceDetails(); updateDeviceDetails();
@ -102,110 +102,120 @@ Item {
} }
} }
ColumnLayout { headerComponent: Component {
id: layout ConnectionHeader {
icon: root.network?.isSecure ? "lock" : "wifi"
anchors.left: parent.left title: root.network?.ssid ?? qsTr("Unknown")
anchors.right: parent.right }
anchors.top: parent.top
spacing: Appearance.spacing.normal
ConnectionHeader {
icon: root.network?.isSecure ? "lock" : "wifi"
title: root.network?.ssid ?? qsTr("Unknown")
}
SectionHeader {
title: qsTr("Connection status")
description: qsTr("Connection settings for this network")
}
SectionContainer {
ToggleRow {
label: qsTr("Connected")
checked: root.network?.active ?? false
toggle.onToggled: {
if (checked) {
NetworkConnection.handleConnect(root.network, root.session, null);
} else {
Nmcli.disconnectFromNetwork();
}
}
}
TextButton {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.normal
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
visible: {
if (!root.network || !root.network.ssid) {
return false;
}
return Nmcli.hasSavedProfile(root.network.ssid);
}
inactiveColour: Colours.palette.m3secondaryContainer
inactiveOnColour: Colours.palette.m3onSecondaryContainer
text: qsTr("Forget Network")
onClicked: {
if (root.network && root.network.ssid) {
if (root.network.active) {
Nmcli.disconnectFromNetwork();
}
Nmcli.forgetNetwork(root.network.ssid);
}
}
}
}
SectionHeader {
title: qsTr("Network properties")
description: qsTr("Additional information")
}
SectionContainer {
contentSpacing: Appearance.spacing.small / 2
PropertyRow {
label: qsTr("SSID")
value: root.network?.ssid ?? qsTr("Unknown")
}
PropertyRow {
showTopMargin: true
label: qsTr("BSSID")
value: root.network?.bssid ?? qsTr("Unknown")
}
PropertyRow {
showTopMargin: true
label: qsTr("Signal strength")
value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A")
}
PropertyRow {
showTopMargin: true
label: qsTr("Frequency")
value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A")
}
PropertyRow {
showTopMargin: true
label: qsTr("Security")
value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A")
}
}
SectionHeader {
title: qsTr("Connection information")
description: qsTr("Network connection details")
}
SectionContainer {
ConnectionInfoSection {
deviceDetails: Nmcli.wirelessDeviceDetails
}
}
} }
sections: [
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader {
title: qsTr("Connection status")
description: qsTr("Connection settings for this network")
}
SectionContainer {
ToggleRow {
label: qsTr("Connected")
checked: root.network?.active ?? false
toggle.onToggled: {
if (checked) {
NetworkConnection.handleConnect(root.network, root.session, null);
} else {
Nmcli.disconnectFromNetwork();
}
}
}
TextButton {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.normal
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
visible: {
if (!root.network || !root.network.ssid) {
return false;
}
return Nmcli.hasSavedProfile(root.network.ssid);
}
inactiveColour: Colours.palette.m3secondaryContainer
inactiveOnColour: Colours.palette.m3onSecondaryContainer
text: qsTr("Forget Network")
onClicked: {
if (root.network && root.network.ssid) {
if (root.network.active) {
Nmcli.disconnectFromNetwork();
}
Nmcli.forgetNetwork(root.network.ssid);
}
}
}
}
}
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader {
title: qsTr("Network properties")
description: qsTr("Additional information")
}
SectionContainer {
contentSpacing: Appearance.spacing.small / 2
PropertyRow {
label: qsTr("SSID")
value: root.network?.ssid ?? qsTr("Unknown")
}
PropertyRow {
showTopMargin: true
label: qsTr("BSSID")
value: root.network?.bssid ?? qsTr("Unknown")
}
PropertyRow {
showTopMargin: true
label: qsTr("Signal strength")
value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A")
}
PropertyRow {
showTopMargin: true
label: qsTr("Frequency")
value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A")
}
PropertyRow {
showTopMargin: true
label: qsTr("Security")
value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A")
}
}
}
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader {
title: qsTr("Connection information")
description: qsTr("Network connection details")
}
SectionContainer {
ConnectionInfoSection {
deviceDetails: Nmcli.wirelessDeviceDetails
}
}
}
}
]
} }

View file

@ -9,6 +9,7 @@ import qs.components.containers
import qs.services import qs.services
import qs.config import qs.config
import qs.utils import qs.utils
import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
@ -58,6 +59,9 @@ DeviceList {
toggled: Nmcli.wifiEnabled toggled: Nmcli.wifiEnabled
icon: "wifi" icon: "wifi"
accent: "Tertiary" accent: "Tertiary"
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
onClicked: { onClicked: {
Nmcli.toggleWifi(null); Nmcli.toggleWifi(null);
@ -68,6 +72,9 @@ DeviceList {
toggled: Nmcli.scanning toggled: Nmcli.scanning
icon: "wifi_find" icon: "wifi_find"
accent: "Secondary" accent: "Secondary"
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
onClicked: { onClicked: {
Nmcli.rescanWifi(); Nmcli.rescanWifi();
@ -78,6 +85,9 @@ DeviceList {
toggled: !root.session.network.active toggled: !root.session.network.active
icon: "settings" icon: "settings"
accent: "Primary" accent: "Primary"
iconSize: Appearance.font.size.normal
horizontalPadding: Appearance.padding.normal
verticalPadding: Appearance.padding.smaller
onClicked: { onClicked: {
if (root.session.network.active) if (root.session.network.active)
@ -94,8 +104,7 @@ DeviceList {
StyledRect { StyledRect {
required property var modelData required property var modelData
anchors.left: parent.left width: ListView.view ? ListView.view.width : undefined
anchors.right: parent.right
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal radius: Appearance.rounding.normal