diff --git a/Helpers/Network/Network.qml b/Helpers/Network/Network.qml new file mode 100644 index 0000000..1e4e4ee --- /dev/null +++ b/Helpers/Network/Network.qml @@ -0,0 +1,122 @@ +pragma Singleton + +import Quickshell +import Quickshell.Networking +import QtQuick + +Singleton { + id: root + + property bool active: false + readonly property list connectedNetworks: networks.filter(n => n.connected) + property Network connectingNetwork + readonly property list devices: Networking.devices.values + property Network failedNetwork + readonly property list knownNetworks: networks.filter(n => n.known && !n.connected) + readonly property list networks: { + const list = []; + for (const d of wifiDevices) { + for (const n of d.networks.values) { + if (!list.includes(n)) + list.push(n); + } + } + return list; + } + property bool scanning: false + readonly property list unknownNetworks: networks.filter(n => !n.known) + readonly property list wifiDevices: devices.filter(d => wifiDevice(d)) + readonly property bool wifiEnabled: Networking.wifiEnabled + + signal networkConnected + + function forgetNetworks(target): void { + const list = Array.isArray(target) ? target : [target]; + for (const n of list) { + if (n) + n.forget(); + } + } + + function isSecure(security): bool { + return (security === WifiSecurityType.WpaPsk || security === WifiSecurityType.Wpa2Psk || security === WifiSecurityType.Sae); + } + + function requestConnect(network: Network, secret = undefined): void { + connectingNetwork = network; + if (secret === undefined || secret === "") { + network.connect(); + } else { + (network as WifiNetwork).connectWithPsk(secret); + } + } + + function setScan(value: bool): void { + for (const d of wifiDevices) { + d.scannerEnabled = value; + } + } + + function setWifi(value: bool): void { + Networking.wifiEnabled = value; + } + + function wifiDevice(dev): bool { + return dev.type === DeviceType.Wifi; + } + + onActiveChanged: { + for (const d of wifiDevices) + d.scannerEnabled = active; + } + + Timer { + id: flushFailed + + interval: 5000 + running: root.failedNetwork + + onTriggered: { + root.failedNetwork = null; + } + } + + Timer { + id: checkConnection + + interval: 500 + repeat: true + running: root.connectingNetwork + + onTriggered: { + var completedNetwork; + + switch (root.connectingNetwork.state) { + case ConnectionState.Connecting: + break; + case ConnectionState.Connected: + completedNetwork = root.connectingNetwork; + root.networkConnected(); + break; + case ConnectionState.Disconnected: + root.failedNetwork = root.connectingNetwork; + completedNetwork = root.connectingNetwork; + break; + } + + if (completedNetwork) { + root.connectingNetwork = null; + } + } + } + + Timer { + interval: 10000 + running: checkConnection.running + + onTriggered: { + root.failedNetwork = root.connectingNetwork; + root.connectingNetwork = null; + } + } +} diff --git a/Helpers/Network/NetworkDivider.qml b/Helpers/Network/NetworkDivider.qml new file mode 100644 index 0000000..5393f40 --- /dev/null +++ b/Helpers/Network/NetworkDivider.qml @@ -0,0 +1,31 @@ +import QtQuick +import ZShell.Config +import qs.Services +import qs.Modules.Settings.Common +import qs.Modules.Bar.Popouts.Network +import qs.Components +import qs.Helpers + +CustomText { + required property var modelData + + bottomPadding: Tokens.spacing.extraSmall + color: Colors.palette.m3onSurfaceVariant + font.pointSize: Tokens.font.size.smaller + text: { + if (modelData.type === "known") { + const count = Network.knownNetworks.filter(n => !n.connected).length; + + return qsTr("%1 known network%2").arg(count).arg(count === 1 ? "" : "s"); + } + + if (modelData.type === "unknown") { + const count = Network.unknownNetworks.filter(n => !n.connected).length; + + return qsTr("%1 network%2 available").arg(count).arg(count === 1 ? "" : "s"); + } + + return qsTr("object unknown"); + } + topPadding: Tokens.spacing.small +} diff --git a/Helpers/Network/NetworkItem.qml b/Helpers/Network/NetworkItem.qml new file mode 100644 index 0000000..23e2232 --- /dev/null +++ b/Helpers/Network/NetworkItem.qml @@ -0,0 +1,116 @@ +import QtQuick +import QtQuick.Layouts +import ZShell.Config +import qs.Modules.Settings.Common +import qs.Components +import qs.Helpers +import qs.Services + +ConnectedRect { + id: networkItem + + readonly property int connectedCount: Network.connectedNetworks.length + readonly property bool connecting: Network.connectingNetwork === modelData + property int horizontalPadding: Tokens.padding.largeIncreased + readonly property bool inConnectedGroup: index < connectedCount + // sectioning first / last item in list reminder + // if index === network.connectedNetworks.length - 1 + // if index === network.connectedNetworks.length - 1 + network.knownNetworks.length - 2 + // if index === network.connectedNetworks.length - 1 + network.knownNetworks.length + // list.model.values.length - 1 + required property int index + readonly property bool isSecure: Network.isSecure(modelData.security) + required property ListView list + required property var modelData + property int verticalPadding: Tokens.padding.smaller + + Layout.fillWidth: true + first: !inConnectedGroup && index === connectedCount + 1 + implicitHeight: rowLayout.implicitHeight + verticalPadding * 2 + last: inConnectedGroup ? index === connectedCount - 1 : index === (list?.count ?? -1) + radius: Tokens.rounding.small + + RowLayout { + id: rowLayout + + Layout.preferredHeight: visible ? rowLayout.implicitHeight + Tokens.padding.smaller * 2 : 0 + anchors.fill: parent + anchors.leftMargin: networkItem.horizontalPadding + anchors.rightMargin: networkItem.horizontalPadding + + Column { + id: column + + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + Layout.leftMargin: Tokens.spacing.extraSmall + Layout.rightMargin: Tokens.spacing.extraSmall + color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurface + elide: Text.ElideRight + text: networkItem.modelData.name + } + + CustomText { + id: subtext + + anchors.left: parent.left + anchors.right: parent.right + color: Colors.palette.m3outline + elide: Text.ElideRight + font.pointSize: Tokens.font.size.small + text: networkItem.modelData.connected ? qsTr("Connected: %1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") : qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") + } + } + + Item { + Layout.alignment: Qt.AlignRight + implicitHeight: icon.height + implicitWidth: icon.width + + MaterialIcon { + id: icon + + color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant + font.pointSize: Tokens.font.size.large + opacity: networkItem.connecting ? 0 : 1 + text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure) + + Behavior on opacity { + Anim { + } + } + } + + Loader { + active: opacity > 0 + anchors.fill: parent + opacity: networkItem.connecting ? 1 : 0 + + Behavior on opacity { + Anim { + } + } + sourceComponent: CircularIndicator { + bgColor: "transparent" + running: true + } + } + } + } + + StateLayer { + color: networkItem.modelData.active ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface + + onClicked: { + if (networkItem.isSecure && !networkItem.known) { + PopupManager.requestOpen(Qt.resolvedUrl("Network/PSKOverlay.qml"), { + network: networkItem.modelData + }); + } else { + Network.requestConnect(networkItem.modelData); + } + } + } +} diff --git a/Helpers/Network/Toggle.qml b/Helpers/Network/Toggle.qml new file mode 100644 index 0000000..5da9ae6 --- /dev/null +++ b/Helpers/Network/Toggle.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Layouts +import ZShell.Config +import qs.Services +import qs.Components +import qs.Modules.Settings.Common + +CustomSwitch { + id: toggleItem + + readonly property alias bg: bg + property alias first: bg.first + property alias last: bg.last + property string subtext + + Layout.fillWidth: true + cLayer: 2 + horizontalPadding: Tokens.padding.largeIncreased + implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2 + implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2 + indicator.anchors.right: right + indicator.anchors.rightMargin: toggleItem.horizontalPadding + indicator.anchors.verticalCenter: verticalCenter + verticalPadding: Tokens.padding.normal + + background: ConnectedRect { + id: bg + + StateLayer { + id: stateLayer + + manualPressOverride: toggleItem.pressed + } + } + contentItem: Item { + anchors.left: parent.left + anchors.leftMargin: toggleItem.horizontalPadding + anchors.right: toggleItem.indicator.left + anchors.rightMargin: Tokens.spacing.normal + implicitHeight: column.implicitHeight + implicitWidth: column.implicitWidth + + ColumnLayout { + id: column + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + CustomText { + id: label + + Layout.fillWidth: true + elide: Text.ElideRight + font.pointSize: Tokens.font.size.smaller + text: toggleItem.text + } + + CustomText { + id: subtext + + Layout.fillWidth: true + color: Colors.palette.m3outline + elide: Text.ElideRight + font.pointSize: Tokens.font.size.small + text: toggleItem.subtext + visible: toggleItem.subtext + } + } + } + + onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY) +} diff --git a/Modules/Bar/Popouts/NetworkPopout.qml b/Modules/Bar/Popouts/NetworkPopout.qml index a005570..c7871c0 100644 --- a/Modules/Bar/Popouts/NetworkPopout.qml +++ b/Modules/Bar/Popouts/NetworkPopout.qml @@ -6,9 +6,10 @@ import Quickshell import ZShell.Components import ZShell.Config import qs.Services +import qs.Modules.Settings.Common import qs.Modules.Bar.Popouts.Network import qs.Components -import qs.Helpers +import qs.Helpers.Network CustomClippingRect { id: root @@ -72,7 +73,7 @@ CustomClippingRect { DelegateChoice { roleValue: true - delegate: StatusDivider { + delegate: NetworkDivider { } } @@ -138,225 +139,4 @@ CustomClippingRect { NetworkOverlay { anchors.fill: parent } - - component ConnectedRect: CustomRect { - id: bg - - property bool first - property bool last - - bottomLeftRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall - bottomRightRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall - color: Colors.tPalette.m3surfaceContainer - topLeftRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall - topRightRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall - } - component NetworkItem: ConnectedRect { - id: networkItem - - readonly property int connectedCount: Network.connectedNetworks.length - readonly property bool connecting: Network.connectingNetwork === modelData - property int horizontalPadding: Tokens.padding.largeIncreased - readonly property bool inConnectedGroup: index < connectedCount - // sectioning first / last item in list reminder - // if index === network.connectedNetworks.length - 1 - // if index === network.connectedNetworks.length - 1 + network.knownNetworks.length - 2 - // if index === network.connectedNetworks.length - 1 + network.knownNetworks.length - // list.model.values.length - 1 - required property int index - readonly property bool isSecure: Network.isSecure(modelData.security) - required property ListView list - required property var modelData - property int verticalPadding: Tokens.padding.smaller - - Layout.fillWidth: true - first: !inConnectedGroup && index === connectedCount + 1 - implicitHeight: rowLayout.implicitHeight + verticalPadding * 2 - last: inConnectedGroup ? index === connectedCount - 1 : index === list.count - 1 - radius: Tokens.rounding.small - - RowLayout { - id: rowLayout - - Layout.preferredHeight: visible ? rowLayout.implicitHeight + Tokens.padding.smaller * 2 : 0 - anchors.fill: parent - anchors.leftMargin: networkItem.horizontalPadding - anchors.rightMargin: networkItem.horizontalPadding - - Column { - id: column - - Layout.fillWidth: true - - CustomText { - Layout.fillWidth: true - Layout.leftMargin: Tokens.spacing.extraSmall - Layout.rightMargin: Tokens.spacing.extraSmall - color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurface - elide: Text.ElideRight - text: networkItem.modelData.name - } - - CustomText { - id: subtext - - anchors.left: parent.left - anchors.right: parent.right - color: Colors.palette.m3outline - elide: Text.ElideRight - font.pointSize: Tokens.font.size.small - text: networkItem.modelData.connected ? qsTr("Connected: %1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") : qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") - } - } - - Item { - Layout.alignment: Qt.AlignRight - implicitHeight: icon.height - implicitWidth: icon.width - - MaterialIcon { - id: icon - - color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large - opacity: networkItem.connecting ? 0 : 1 - text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure) - - Behavior on opacity { - Anim { - } - } - } - - Loader { - active: opacity > 0 - anchors.fill: parent - opacity: networkItem.connecting ? 1 : 0 - - Behavior on opacity { - Anim { - } - } - sourceComponent: CircularIndicator { - bgColor: "transparent" - running: true - } - } - } - } - - StateLayer { - color: networkItem.modelData.active ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface - - onClicked: { - if (networkItem.isSecure && !networkItem.known) { - PopupManager.requestOpen(Qt.resolvedUrl("Network/PSKOverlay.qml"), { - network: networkItem.modelData - }); - } else { - Network.requestConnect(networkItem.modelData); - } - } - } - } - component StatusDivider: CustomText { - required property var modelData - - bottomPadding: Tokens.spacing.extraSmall - color: Colors.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.smaller - text: { - if (modelData.type === "known") { - const count = Network.knownNetworks.filter(n => !n.connected).length; - - return qsTr("%1 known network%2").arg(count).arg(count === 1 ? "" : "s"); - } - - if (modelData.type === "unknown") { - const count = Network.unknownNetworks.filter(n => !n.connected).length; - - return qsTr("%1 network%2 available").arg(count).arg(count === 1 ? "" : "s"); - } - - return qsTr("object unknown"); - } - topPadding: Tokens.spacing.small - } - component Toggle: CustomSwitch { - id: toggleItem - - readonly property alias bg: bg - property alias first: bg.first - property alias last: bg.last - property string subtext - - Layout.fillWidth: true - cLayer: 2 - horizontalPadding: Tokens.padding.largeIncreased - implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2 - implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2 - indicator.anchors.right: right - indicator.anchors.rightMargin: toggleItem.horizontalPadding - indicator.anchors.verticalCenter: verticalCenter - verticalPadding: Tokens.padding.normal - - background: ConnectedRect { - id: bg - - StateLayer { - id: stateLayer - - manualPressOverride: toggleItem.pressed - } - } - contentItem: Item { - anchors.left: parent.left - anchors.leftMargin: toggleItem.horizontalPadding - anchors.right: toggleItem.indicator.left - anchors.rightMargin: Tokens.spacing.normal - implicitHeight: column.implicitHeight - implicitWidth: column.implicitWidth - - Column { - id: column - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: 0 - - CustomText { - id: label - - anchors.left: parent.left - anchors.right: parent.right - elide: Text.ElideRight - font: { - const f = Qt.font(label.font); - f.pointSize = Tokens.font.size.smaller; - return f; - } - text: toggleItem.text - } - - CustomText { - id: subtext - - anchors.left: parent.left - anchors.right: parent.right - color: Colors.palette.m3outline - elide: Text.ElideRight - font: { - const f = Qt.font(subtext.font); - f.pointSize = Tokens.font.size.small; - return f; - } - text: toggleItem.subtext - visible: toggleItem.subtext - } - } - } - - onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY) - } } diff --git a/Modules/Settings/Common/ConnectedRect.qml b/Modules/Settings/Common/ConnectedRect.qml index ec422f9..c4bb695 100644 --- a/Modules/Settings/Common/ConnectedRect.qml +++ b/Modules/Settings/Common/ConnectedRect.qml @@ -17,7 +17,7 @@ CustomRect { bottomLeftRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall bottomRightRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall color: Colors.tPalette.m3surfaceContainer - opacity: enabled && parent.enabled ? 1 : 0.5 + opacity: enabled && (!parent || parent.enabled) ? 1 : 0.5 topLeftRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall topRightRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall diff --git a/Modules/Settings/Pages/NetworkPage.qml b/Modules/Settings/Pages/NetworkPage.qml index 57e3ce3..5e1c749 100644 --- a/Modules/Settings/Pages/NetworkPage.qml +++ b/Modules/Settings/Pages/NetworkPage.qml @@ -7,7 +7,7 @@ import ZShell.Config import qs.Modules.Settings.Common import qs.Modules.Bar.Popouts.Network import qs.Components -import qs.Helpers +import qs.Helpers.Network import qs.Services PageBase { @@ -27,214 +27,206 @@ PageBase { Layout.preferredHeight: visible ? implicitHeight : 0 checked: Network.wifiEnabled first: true - last: connected.count === 0 + last: Network.connectedNetworks.length === 0 subtext: qsTr("Toggle WiFi device state") text: checked ? qsTr("Enabled") : qsTr("Disabled") onToggled: Network.setWifi(checked) } - Repeater { - id: connected + ListView { + id: rows - visible: Network.connectedNetworks.length > 0 + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + interactive: false + section.property: "connected" + spacing: Tokens.spacing.extraSmall / 2 + add: Transition { + Anim { + from: 0 + property: "opacity" + to: 1 + } + } + delegate: DelegateChooser { + role: "isDivider" + + DelegateChoice { + roleValue: true + + delegate: NetworkDivider { + } + } + + DelegateChoice { + delegate: NetworkItem { + list: rows + width: rows.width + } + } + } + displaced: Transition { + Anim { + easing.type: Easing.OutCubic + property: "y" + } + } model: ScriptModel { - values: [...Network.connectedNetworks].sort((a, b) => { - return b.signalStrength - a.signalStrength; - }) + values: { + const byStrengthName = (a, b) => b.signalStrength - a.signalStrength || a.name.localeCompare(b.name); + const cNetworks = [...Network.connectedNetworks]; + const kNetworks = [...Network.knownNetworks]; + const uNetworks = [...Network.unknownNetworks]; + + return [...cNetworks.sort(byStrengthName), ...(kNetworks.length > 0 ? [ + { + isDivider: true, + type: "known" + }, + ...kNetworks.sort(byStrengthName)] : []), ...(uNetworks.length > 0 ? [ + { + isDivider: true, + type: "unknown" + }, + ...uNetworks.sort(byStrengthName)] : [])]; + } } - - NetworkItem { - first: false - known: true - repeater: connected + move: Transition { + Anim { + easing.type: Easing.OutCubic + property: "y" + } } - } - } + remove: Transition { + Anim { + property: "opacity" + to: 0 + } + } + section.delegate: Rectangle { + required property string section - component NetworkItem: ConnectedRect { - id: networkItem - - readonly property bool connecting: Network.connectingNetwork === modelData - property int horizontalPadding: Tokens.padding.largeIncreased - required property int index - readonly property bool isSecure: Network.isSecure(modelData.security) - required property bool known - required property var modelData - required property Repeater repeater - - Layout.fillWidth: true - Layout.preferredHeight: visible ? rowLayout.implicitHeight + Tokens.padding.smaller * 2 : 0 - first: index === 0 - last: index === (repeater.count - 1) - radius: Tokens.rounding.small - - RowLayout { - id: rowLayout - - anchors.fill: parent - anchors.leftMargin: networkItem.horizontalPadding - anchors.rightMargin: networkItem.horizontalPadding - - Column { - id: column - - Layout.fillWidth: true + color: "transparent" + height: childrenRect.height + width: ListView.view.width CustomText { - Layout.fillWidth: true - Layout.leftMargin: Tokens.spacing.extraSmall - Layout.rightMargin: Tokens.spacing.extraSmall - color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurface - elide: Text.ElideRight - text: networkItem.modelData.name - } - - CustomText { - id: subtext - - anchors.left: parent.left - anchors.right: parent.right - color: Colors.palette.m3outline - elide: Text.ElideRight - font.pointSize: Tokens.font.size.small - text: qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") - } - } - - Item { - Layout.alignment: Qt.AlignRight - implicitHeight: icon.height - implicitWidth: icon.width - - MaterialIcon { - id: icon - - color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large - opacity: networkItem.connecting ? 0 : 1 - text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure) - - Behavior on opacity { - Anim { - } - } - } - - Loader { - active: opacity > 0 - anchors.fill: parent - opacity: networkItem.connecting ? 1 : 0 - - Behavior on opacity { - Anim { - } - } - sourceComponent: CircularIndicator { - bgColor: "transparent" - running: true - } + text: parent.section } } } - StateLayer { - color: networkItem.modelData.active ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface + KNetworkList { + acceptLabel: qsTr("Remove") + enabled: Object.keys(model).length > 0 + header: qsTr("Manage known networks") + icon: "Network_Manage" + label: qsTr("Show all known networks") + model: Network.networks.filter(n => n.known && !n.connected).map(n => ({ + id: n.id, + label: n.name + })) + rootParent: root.flickable - onClicked: { - if (networkItem.isSecure && !networkItem.known) { - PopupManager.requestOpen(Qt.resolvedUrl("Network/PSKOverlay.qml"), { - network: networkItem.modelData - }); - } else { - Network.requestConnect(networkItem.modelData); - } + onAccepted: { + if (!selectedItem) + return; + + Network.forgetNetworks(Network.networks.find(n => n.id === selectedItem)); } } } - component Spacer: Item { - id: spacer - required property Repeater repeater + component KNetworkList: DialogRowButton { + id: klist - Layout.preferredHeight: repeater.count > 0 ? Tokens.spacing.extraSmall : 0 - } - component Toggle: CustomSwitch { - id: toggleItem + required property var model + property var selectedItem - readonly property alias bg: bg - property alias first: bg.first - property alias last: bg.last - property string subtext - - Layout.fillWidth: true - cLayer: 2 - horizontalPadding: Tokens.padding.largeIncreased - implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2 - implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2 - indicator.anchors.right: right - indicator.anchors.rightMargin: toggleItem.horizontalPadding - indicator.anchors.verticalCenter: verticalCenter - verticalPadding: Tokens.padding.normal - - background: ConnectedRect { - id: bg - - StateLayer { - id: stateLayer - - manualPressOverride: toggleItem.pressed - } + function keyFor(item: var): string { + return item.id; } - contentItem: Item { - anchors.left: parent.left - anchors.leftMargin: toggleItem.horizontalPadding - anchors.right: toggleItem.indicator.left - anchors.rightMargin: Tokens.spacing.normal - implicitHeight: column.implicitHeight - implicitWidth: column.implicitWidth - Column { - id: column + function labelFor(item: var): string { + return item.label; + } - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + acceptAllowed: !!selectedItem + horizontalContentMargin: -Tokens.padding.small + separateContent: true + + content: Component { + VerticalFadeListView { + bottomMargin: Tokens.padding.large + model: klist.model spacing: 0 + topMargin: Tokens.padding.large - CustomText { - id: label + delegate: CustomRect { + id: item - anchors.left: parent.left - anchors.right: parent.right - elide: Text.ElideRight - font: { - const f = Qt.font(label.font); - f.pointSize = Tokens.font.size.smaller; - return f; + required property var modelData + readonly property bool selected: klist.selectedItem === klist.keyFor(modelData) + + anchors.left: ListView.view.contentItem.left + anchors.margins: 1 // Gets cut off for some reason without this + anchors.right: ListView.view.contentItem.right + color: Qt.alpha(Colors.palette.m3tertiaryContainer, selected ? 1 : 0) + implicitHeight: label.implicitHeight + Tokens.padding.normal * 2 + radius: stateLayer.pressed ? Tokens.rounding.extraSmall : selected ? Tokens.rounding.large : Tokens.rounding.normal + + Behavior on radius { + Anim { + type: Anim.SlowEffects + } } - text: toggleItem.text - } - CustomText { - id: subtext + StateLayer { + id: stateLayer - anchors.left: parent.left - anchors.right: parent.right - color: Colors.palette.m3outline - elide: Text.ElideRight - font: { - const f = Qt.font(subtext.font); - f.pointSize = Tokens.font.size.small; - return f; + onClicked: klist.selectedItem = klist.keyFor(item.modelData) + } + + CustomText { + id: label + + anchors.left: parent.left + anchors.margins: Tokens.padding.large + anchors.right: item.selected ? checkIcon.left : parent.right + anchors.rightMargin: item.selected ? Tokens.spacing.normal : anchors.margins + anchors.verticalCenter: parent.verticalCenter + color: item.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface + elide: Text.ElideRight + font.pointSize: Tokens.font.size.smaller + text: klist.labelFor(item.modelData) + } + + MaterialIcon { + id: checkIcon + + anchors.margins: Tokens.padding.large + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: Colors.palette.m3onTertiaryContainer + font.pointSize: Tokens.font.size.medium + opacity: item.selected ? 1 : 0 + text: "check" + + Behavior on opacity { + Anim { + type: Anim.SlowEffects + } + } } - text: toggleItem.subtext - visible: toggleItem.subtext } } } - onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY) + onOpenChanged: { + if (open) + selectedItem = null; + } } }