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,10 +24,25 @@ SplitPaneWithDetails {
} }
leftContent: Component { leftContent: Component {
StyledFlickable {
id: leftFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: deviceList.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: leftFlickable
}
DeviceList { DeviceList {
id: deviceList
anchors.left: parent.left
anchors.right: parent.right
session: root.session session: root.session
} }
} }
}
rightDetailsComponent: Component { rightDetailsComponent: Component {
Details { Details {

View file

@ -20,32 +20,41 @@ 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.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
implicitHeight: layout.height implicitHeight: details.implicitHeight
ColumnLayout { DeviceDetails {
id: layout 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
spacing: Appearance.spacing.normal
session: root.session
device: root.device
headerComponent: Component {
SettingsHeader { SettingsHeader {
icon: Icons.getBluetoothIcon(root.device?.icon ?? "") icon: Icons.getBluetoothIcon(root.device?.icon ?? "")
title: root.device?.name ?? "" title: root.device?.name ?? ""
} }
}
sections: [
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
StyledText { StyledText {
Layout.topMargin: Appearance.spacing.large Layout.topMargin: Appearance.spacing.large
@ -100,6 +109,11 @@ StyledFlickable {
} }
} }
} }
}
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
StyledText { StyledText {
Layout.topMargin: Appearance.spacing.large Layout.topMargin: Appearance.spacing.large
@ -304,6 +318,11 @@ StyledFlickable {
} }
} }
} }
}
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
StyledText { StyledText {
Layout.topMargin: Appearance.spacing.large Layout.topMargin: Appearance.spacing.large
@ -417,7 +436,11 @@ StyledFlickable {
} }
} }
} }
]
}
}
// 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,41 +11,39 @@ 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
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: Appearance.spacing.normal
ConnectionHeader { ConnectionHeader {
icon: "cable" icon: "cable"
title: root.device?.interface ?? qsTr("Unknown") title: root.ethernetDevice?.interface ?? qsTr("Unknown")
} }
}
sections: [
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader { SectionHeader {
title: qsTr("Connection status") title: qsTr("Connection status")
@ -54,18 +53,23 @@ Item {
SectionContainer { SectionContainer {
ToggleRow { ToggleRow {
label: qsTr("Connected") label: qsTr("Connected")
checked: root.device?.connected ?? false checked: root.ethernetDevice?.connected ?? false
toggle.onToggled: { toggle.onToggled: {
if (checked) { if (checked) {
Nmcli.connectEthernet(root.device?.connection || "", root.device?.interface || "", () => {}); Nmcli.connectEthernet(root.ethernetDevice?.connection || "", root.ethernetDevice?.interface || "", () => {});
} else { } else {
if (root.device?.connection) { if (root.ethernetDevice?.connection) {
Nmcli.disconnectEthernet(root.device.connection, () => {}); Nmcli.disconnectEthernet(root.ethernetDevice.connection, () => {});
} }
} }
} }
} }
} }
}
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader { SectionHeader {
title: qsTr("Device properties") title: qsTr("Device properties")
@ -77,21 +81,26 @@ Item {
PropertyRow { PropertyRow {
label: qsTr("Interface") label: qsTr("Interface")
value: root.device?.interface ?? qsTr("Unknown") value: root.ethernetDevice?.interface ?? qsTr("Unknown")
} }
PropertyRow { PropertyRow {
showTopMargin: true showTopMargin: true
label: qsTr("Connection") label: qsTr("Connection")
value: root.device?.connection || qsTr("Not connected") value: root.ethernetDevice?.connection || qsTr("Not connected")
} }
PropertyRow { PropertyRow {
showTopMargin: true showTopMargin: true
label: qsTr("State") label: qsTr("State")
value: root.device?.state ?? qsTr("Unknown") value: root.ethernetDevice?.state ?? qsTr("Unknown")
} }
} }
}
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader { SectionHeader {
title: qsTr("Connection information") title: qsTr("Connection information")
@ -104,5 +113,6 @@ Item {
} }
} }
} }
}
]
} }

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,7 +28,6 @@ Item {
anchors.fill: parent anchors.fill: parent
leftContent: Component { leftContent: Component {
StyledFlickable { StyledFlickable {
id: leftFlickable id: leftFlickable
@ -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,18 +102,17 @@ Item {
} }
} }
ColumnLayout { headerComponent: Component {
id: layout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: Appearance.spacing.normal
ConnectionHeader { ConnectionHeader {
icon: root.network?.isSecure ? "lock" : "wifi" icon: root.network?.isSecure ? "lock" : "wifi"
title: root.network?.ssid ?? qsTr("Unknown") title: root.network?.ssid ?? qsTr("Unknown")
} }
}
sections: [
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader { SectionHeader {
title: qsTr("Connection status") title: qsTr("Connection status")
@ -157,6 +156,11 @@ Item {
} }
} }
} }
}
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader { SectionHeader {
title: qsTr("Network properties") title: qsTr("Network properties")
@ -195,6 +199,11 @@ Item {
value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A") value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A")
} }
} }
}
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader { SectionHeader {
title: qsTr("Connection information") title: qsTr("Connection information")
@ -207,5 +216,6 @@ Item {
} }
} }
} }
}
]
} }

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