refactor: use entries directly in bar object instead of nested components + various fixes and additions to settings
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 23s
Python / lint (pull_request) Successful in 30s
Python / fmt (pull_request) Successful in 32s
Python / test (pull_request) Successful in 1m1s
Python / typecheck (pull_request) Failing after 52s
C++ / build (pull_request) Successful in 1m45s
Rust / fmt (pull_request) Successful in 57s
Rust / build (pull_request) Successful in 1m50s
Rust / clippy (pull_request) Successful in 1m44s
Python / buildcheck (pull_request) Successful in 2m29s
C++ / clang-tidy (pull_request) Successful in 3m31s

This commit is contained in:
2026-08-11 14:05:57 +02:00
parent 93c6ac251e
commit d81808dd8f
20 changed files with 1321 additions and 324 deletions
@@ -16,9 +16,15 @@ CustomRect {
bottomLeftRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall
bottomRightRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall
color: DynamicColors.tPalette.m3surfaceContainer
opacity: enabled && parent.enabled ? 1 : 0.5
topLeftRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall
topRightRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall
Behavior on opacity {
Anim {
}
}
CustomRect {
id: highlight
+249
View File
@@ -0,0 +1,249 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Blobs
import qs.Components
import qs.Config
Item {
id: root
property bool acceptAllowed: true
required property string acceptLabel
required property Component content
required property string header
property int horizontalContentMargin
required property string icon
required property string label
property bool open
property real openHeight: Math.min(rootParent.height * 0.8, 600)
property real openWidth: Math.min(rootParent.width * 0.8, 400)
required property Item rootParent
property bool separateContent
signal accepted
signal cancelled
function reparentWrapper(): void {
const newParent = open ? rootParent : root;
const pos = dialogWrapper.mapToItem(newParent, 0, 0);
dialogWrapper.parent = newParent;
dialogWrapper.x = pos.x;
dialogWrapper.y = pos.y;
}
Layout.fillWidth: true
implicitHeight: openButton.implicitHeight
z: open || dialogTransition.running ? 2 : 0
BlobGroup {
id: blobGroup
color: root.open ? DynamicColors.palette.m3surfaceContainerHighest : DynamicColors.tPalette.m3surfaceContainer
Behavior on color {
CAnim {
}
}
}
MouseArea {
id: backdrop
anchors.fill: parent
enabled: false
hoverEnabled: enabled
parent: root.open ? root.rootParent : root
onClicked: root.open = false
}
Item {
id: dialogWrapper
height: openButton.implicitHeight
width: root.width
z: 1
states: State {
name: "open"
when: root.open
PropertyChanges {
backdrop.enabled: true
dialogBg.bottomLeftRadius: Appearance.rounding.large
dialogBg.bottomRightRadius: Appearance.rounding.large
dialogBg.radius: Appearance.rounding.large
dialogContent.opacity: 1
dialogWrapper.height: root.openHeight
dialogWrapper.width: root.openWidth
dialogWrapper.x: (root.rootParent.width - root.openWidth) / 2
dialogWrapper.y: (root.rootParent.height - root.openHeight) / 2
elevation.opacity: 1
openButton.opacity: 0
}
}
transitions: Transition {
id: dialogTransition
SequentialAnimation {
ScriptAction {
script: root.reparentWrapper()
}
Anim {
properties: "x,y"
}
}
PropertyAction {
property: "enabled"
}
Anim {
properties: "opacity,radius,bottomLeftRadius,bottomRightRadius"
type: Anim.DefaultEffects
}
Anim {
properties: "width,height"
}
}
Elevation {
id: elevation
anchors.fill: parent
bottomLeftRadius: dialogBg.bottomLeftRadius
bottomRightRadius: dialogBg.bottomRightRadius
level: 4
opacity: 0
radius: dialogBg.radius
transform: Matrix4x4 {
matrix: dialogBg.deformMatrix
}
}
BlobRect {
id: dialogBg
anchors.fill: parent
bottomLeftRadius: Appearance.rounding.large
bottomRightRadius: Appearance.rounding.large
deformScale: 0
group: blobGroup
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
radius: Appearance.rounding.extraSmall
}
RowButton {
id: openButton
anchors.left: parent.left
anchors.right: parent.right
color: "transparent"
height: Math.min(implicitHeight, parent.height) // Clamp to parent height due to overshoot anim
icon: root.icon
last: true
text: root.label
transform: Matrix4x4 {
matrix: dialogBg.deformMatrix
}
onClicked: root.open = true
}
Loader {
id: dialogContent
active: opacity > 0
anchors.fill: parent
asynchronous: true
opacity: 0
sourceComponent: MouseArea {
onWheel: event => event.accepted = true
ColumnLayout {
anchors.bottomMargin: Appearance.padding.largeIncreased
anchors.fill: parent
anchors.margins: Appearance.padding.extraLarge
spacing: 0
CustomText {
font.pointSize: Appearance.font.size.large
text: root.header
}
Loader {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.normal
active: root.separateContent
sourceComponent: CustomRect {
color: DynamicColors.palette.m3outline
implicitHeight: 1
}
}
Loader {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: root.horizontalContentMargin
Layout.rightMargin: root.horizontalContentMargin
sourceComponent: root.content
}
Loader {
Layout.bottomMargin: Appearance.spacing.normal
Layout.fillWidth: true
active: root.separateContent
sourceComponent: CustomRect {
color: DynamicColors.palette.m3outline
implicitHeight: 1
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
spacing: Appearance.spacing.extraSmall
TextButton {
horizontalPadding: Appearance.padding.largeIncreased
isRound: true
text: qsTr("Cancel")
type: TextButton.Text
verticalPadding: Appearance.padding.normal
onClicked: {
root.cancelled();
root.open = false;
}
}
TextButton {
enabled: root.acceptAllowed
horizontalPadding: Appearance.padding.largeIncreased
isRound: true
text: root.acceptLabel
type: TextButton.Text
verticalPadding: Appearance.padding.normal
onClicked: {
root.accepted();
root.open = false;
}
}
}
}
}
transform: Matrix4x4 {
matrix: dialogBg.deformMatrix
}
}
}
}
@@ -0,0 +1,96 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Config
import qs.Components
DialogRowButton {
id: root
required property var model
property var selectedItem
function keyFor(item: var): string {
return item.id;
}
function labelFor(item: var): string {
return item.label;
}
acceptAllowed: !!selectedItem
horizontalContentMargin: -Appearance.padding.small
separateContent: true
content: Component {
VerticalFadeListView {
bottomMargin: Appearance.padding.large
model: root.model
spacing: 0
topMargin: Appearance.padding.large
delegate: CustomRect {
id: item
required property var modelData
readonly property bool selected: root.selectedItem === root.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(DynamicColors.palette.m3tertiaryContainer, selected ? 1 : 0)
implicitHeight: label.implicitHeight + Appearance.padding.normal * 2
radius: stateLayer.pressed ? Appearance.rounding.extraSmall : selected ? Appearance.rounding.large : Appearance.rounding.normal
Behavior on radius {
Anim {
type: Anim.SlowEffects
}
}
StateLayer {
id: stateLayer
onClicked: root.selectedItem = root.keyFor(item.modelData)
}
CustomText {
id: label
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.right: item.selected ? checkIcon.left : parent.right
anchors.rightMargin: item.selected ? Appearance.spacing.normal : anchors.margins
anchors.verticalCenter: parent.verticalCenter
color: item.selected ? DynamicColors.palette.m3onTertiaryContainer : DynamicColors.palette.m3onSurface
elide: Text.ElideRight
font.pointSize: Appearance.font.size.smaller
text: root.labelFor(item.modelData)
}
MaterialIcon {
id: checkIcon
anchors.margins: Appearance.padding.large
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.palette.m3onTertiaryContainer
font.pointSize: Appearance.font.size.medium
opacity: item.selected ? 1 : 0
text: "check"
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
}
}
}
onOpenChanged: {
if (open)
selectedItem = null;
}
}
+317
View File
@@ -0,0 +1,317 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Config
import qs.Components
ListView {
id: root
property bool first
property alias values: valuesModel.values
signal itemMoved(from: int, to: int)
signal itemRemoved(index: int)
signal itemToggled(index: int, checked: bool)
function dampOvershoot(overshoot: real, maxOvershoot: real): real {
return overshoot / (1 + overshoot / maxOvershoot);
}
function labelFor(item: var): string {
return item.label;
}
function toggledFor(item: var): bool {
return false;
}
Layout.fillWidth: true
implicitHeight: contentHeight
interactive: false
spacing: Appearance.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
displaced: Transition {
Anim {
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
properties: "y"
}
}
Behavior on implicitHeight {
Anim {
}
}
model: DelegateModel {
id: visualModel
delegate: ListRow {
}
model: ScriptModel {
id: valuesModel
}
}
move: Transition {
Anim {
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
properties: "y"
}
}
remove: Transition {
Anim {
properties: "opacity"
to: 0
type: Anim.DefaultEffects
}
}
component ListRow: Item {
id: item
property bool held
property real lastMoveY
required property var modelData
property int pressIndex
property point pressPos
property real radiusLerpProg
property real topRadius: root?.first && DelegateModel.itemsIndex === 0 ? Appearance.rounding.large : Appearance.rounding.extraSmall
function lerpRadius(a: real, b: real): real {
return a + (b - a) * radiusLerpProg;
}
anchors.left: root?.contentItem.left
anchors.right: root?.contentItem.right
implicitHeight: row.implicitHeight + row.anchors.margins * 2
state: held ? "held" : ""
z: held || returnAnim.running ? 1 : 0
states: State {
name: "held"
PropertyChanges {
dragIcon.color: DynamicColors.palette.m3onPrimaryContainer
elevation.opacity: 1
item.radiusLerpProg: 1
itemBg.color: DynamicColors.palette.m3primaryContainer
label.color: DynamicColors.palette.m3onPrimaryContainer
placeholder.opacity: 0.1
}
}
Behavior on topRadius {
Anim {
type: Anim.DefaultEffects
}
}
transitions: Transition {
Anim {
properties: "opacity,radiusLerpProg"
type: Anim.SlowEffects
}
PropertyAction {
properties: "color,inactiveOnColor"
}
}
CustomRect {
id: placeholder
anchors.fill: parent
color: DynamicColors.palette.m3primary
opacity: 0
radius: Appearance.rounding.extraSmall
topLeftRadius: item.topRadius
topRightRadius: item.topRadius
}
MouseArea {
id: mouse
anchors.fill: parent
preventStealing: true
onPositionChanged: e => {
item.lastMoveY = item.y;
const absY = item.mapToItem(root.contentItem, 0, e.y).y;
const swap = root.itemAt(0, absY);
if (!swap || swap === item)
return;
const idx = item.DelegateModel.itemsIndex;
const swapIdx = swap.DelegateModel.itemsIndex;
visualModel.items.move(idx, swapIdx);
}
onPressed: e => {
returnAnim.stop();
item.pressPos = Qt.point(e.x, e.y);
item.pressIndex = item.DelegateModel.itemsIndex;
item.lastMoveY = item.y;
item.held = true;
itemContent.x = Qt.binding(() => {
if (!root)
return 0;
const maxOvershoot = Appearance.padding.extraExtraLarge;
const x = mouse.mouseX - item.pressPos.x;
return root.dampOvershoot(Math.abs(x), maxOvershoot) * Math.sign(x);
});
itemContent.y = Qt.binding(() => {
if (!root)
return 0;
const maxOvershoot = Appearance.padding.extraLarge;
const yDiff = item.y - item.lastMoveY; // Extra offset if the y of the item changed between mouseY updates
const y = mouse.mouseY - item.pressPos.y - yDiff;
const absY = item.mapToItem(root.contentItem, 0, y).y;
const maxY = root.implicitHeight - item.implicitHeight;
if (absY < 0)
return y - absY - root.dampOvershoot(-absY, maxOvershoot);
if (absY > maxY)
return y - absY + maxY + root.dampOvershoot(absY - maxY, maxOvershoot);
return y;
});
}
onReleased: e => {
// Break bindings
itemContent.x = itemContent.x;
itemContent.y = itemContent.y;
returnAnim.start();
item.held = false;
const idx = item.DelegateModel.itemsIndex;
if (idx !== item.pressIndex)
root.itemMoved(item.pressIndex, idx);
}
}
Anim {
id: returnAnim
properties: "x,y"
target: itemContent
to: 0
}
Item {
id: itemContent
implicitHeight: item.implicitHeight
implicitWidth: item.width
Elevation {
id: elevation
anchors.fill: parent
level: 3
opacity: 0
radius: itemBg.radius
topLeftRadius: itemBg.topLeftRadius
topRightRadius: itemBg.topRightRadius
}
CustomRect {
id: itemBg
anchors.fill: parent
color: DynamicColors.tPalette.m3surfaceContainer
radius: item.lerpRadius(Appearance.rounding.extraSmall, Appearance.rounding.large)
topLeftRadius: item.lerpRadius(item.topRadius, Appearance.rounding.large)
topRightRadius: item.lerpRadius(item.topRadius, Appearance.rounding.large)
}
MouseArea {
id: stateLayer
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: mouse.pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
hoverEnabled: true
CustomRect {
anchors.fill: parent
color: DynamicColors.palette.m3onSurface
opacity: parent.containsMouse && !item.held ? 0.08 : 0
radius: itemBg.radius
topLeftRadius: itemBg.topLeftRadius
topRightRadius: itemBg.topRightRadius
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
}
RowLayout {
id: row
anchors.fill: parent
anchors.leftMargin: Appearance.padding.largeIncreased
anchors.margins: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.large
spacing: Appearance.spacing.normal
MaterialIcon {
id: dragIcon
color: Qt.alpha(DynamicColors.palette.m3onSurfaceVariant, enabledSwitch.checked ? 1 : 0.5)
font.pointSize: Appearance.font.size.medium
text: "drag_indicator"
}
CustomText {
id: label
Layout.fillWidth: true
color: Qt.alpha(DynamicColors.palette.m3onSurface, enabledSwitch.checked ? 1 : 0.5)
elide: Text.ElideRight
font.pointSize: Appearance.font.size.smaller
text: root.labelFor(item.modelData)
}
CustomSwitch {
id: enabledSwitch
checked: root.toggledFor(item.modelData)
font.pointSize: Appearance.font.size.medium
onToggled: root.itemToggled(item.DelegateModel.itemsIndex, checked)
}
IconButton {
font.pointSize: Appearance.font.size.large
icon: "delete"
inactiveOnColor: DynamicColors.palette.m3error
isRound: true
label.fill: 0
type: IconButton.Text
onClicked: root.itemRemoved(item.DelegateModel.itemsIndex)
}
}
}
}
}
+93
View File
@@ -0,0 +1,93 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Config
import qs.Components
ConnectedRect {
id: root
property alias icon: iconLabel.text
readonly property alias iconLabel: iconLabel
readonly property alias label: label
property alias stateEnabled: stateLayer.enabled
readonly property alias subLabel: subLabel
property alias subtext: subLabel.text
property alias text: label.text
property string trailingIcon
signal clicked(event: MouseEvent)
Layout.fillWidth: true
implicitHeight: row.implicitHeight + Appearance.padding.normal * 2
StateLayer {
id: stateLayer
onClicked: e => root.clicked(e)
}
RowLayout {
id: row
anchors.left: parent.left
anchors.margins: Appearance.padding.largeIncreased
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
opacity: root.enabled ? 1 : 0.5
spacing: Appearance.spacing.normal
Behavior on opacity {
Anim {
}
}
MaterialIcon {
id: iconLabel
color: DynamicColors.palette.m3onSurfaceVariant
fill: 1
font.pointSize: Appearance.font.size.medium
}
Column {
id: column
Layout.fillWidth: true
spacing: 0
CustomText {
id: label
anchors.left: parent.left
anchors.right: parent.right
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
}
CustomText {
id: subLabel
anchors.left: parent.left
anchors.right: parent.right
color: DynamicColors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
visible: text
}
}
Loader {
active: root.trailingIcon
asynchronous: true
visible: active
sourceComponent: MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.medium
text: root.trailingIcon
}
}
}
}
+12
View File
@@ -62,7 +62,13 @@ CustomSwitch {
f.pointSize = Appearance.font.size.smaller;
return f;
}
opacity: root.enabled ? 1 : 0.5
text: root.text
Behavior on opacity {
Anim {
}
}
}
CustomText {
@@ -77,8 +83,14 @@ CustomSwitch {
f.pointSize = Appearance.font.size.small;
return f;
}
opacity: root.enabled ? 1 : 0.5
text: root.subtext
visible: root.subtext
Behavior on opacity {
Anim {
}
}
}
}
}
@@ -7,6 +7,12 @@ import qs.Modules.Settings.Common
PageBase {
id: root
readonly property var builtinIcons: ({
audio: qsTr("Speakers"),
microphone: qsTr("Microphone"),
power: qsTr("Battery")
})
isSubPage: true
title: qsTr("System icons")
@@ -22,77 +28,89 @@ PageBase {
text: qsTr("Visible icons")
}
ToggleRow {
checked: Config.bar.tray.showAudio
ListEditor {
function labelFor(item: var): string {
const prettyName = root.builtinIcons[item.id];
if (prettyName)
return prettyName;
const label = item.id.replace(/([A-Z])/g, " $1");
return label.charAt(0).toUpperCase() + label.slice(1).toLowerCase();
}
function toggledFor(item: var): bool {
return item.enabled;
}
first: true
settingAnchor: "bar-status-speakers"
text: qsTr("Speakers")
values: Config.bar.tray.statusIcons.map(item => JSON.parse(JSON.stringify(item)))
z: 1
onToggled: Config.bar.tray.showAudio = checked
onItemMoved: (from, to) => {
const icons = Config.bar.tray.statusIcons.slice();
const [moved] = icons.splice(from, 1);
icons.splice(to, 0, moved);
Config.bar.tray.statusIcons = icons;
}
onItemRemoved: index => {
const icons = Config.bar.tray.statusIcons.slice();
icons.splice(index, 1);
Config.bar.tray.statusIcons = icons;
}
onItemToggled: (index, checked) => {
const icons = Config.bar.tray.statusIcons.slice();
icons[index] = Object.assign({}, icons[index], {
enabled: checked
});
Config.bar.tray.statusIcons = icons;
}
}
ToggleRow {
checked: Config.bar.tray.showMicrophone
settingAnchor: "bar-status-mic"
text: qsTr("Microphone")
DialogSelectButton {
id: addItemContainer
onToggled: Config.bar.tray.showMicrophone = checked
acceptLabel: qsTr("Add")
enabled: {
console.log(Object.keys(model).length);
return Object.keys(model).length > 0;
}
header: qsTr("Add new entry")
icon: "add"
label: qsTr("Add entry")
model: {
const present = new Set(Config.bar.tray.statusIcons.map(item => item.id));
return Object.keys(root.builtinIcons).filter(id => !present.has(id)).map(id => ({
id: id,
label: root.builtinIcons[id]
}));
}
rootParent: root.flickable
onAccepted: {
if (!selectedItem)
return;
const icons = Config.bar.tray.statusIcons.slice();
icons.push({
id: selectedItem,
enabled: true
});
Config.bar.tray.statusIcons = icons;
}
}
//////// FOR LATER:
// ToggleRow {
// checked: Config.bar.tray.showNetwork
// text: qsTr("Network")
//
// onToggled: Config.bar.tray.showNetwork = checked
// }
//
// ToggleRow {
// checked: Config.bar.tray.showWifi
// text: qsTr("Wi-Fi")
//
// onToggled: Config.bar.tray.showWifi = checked
// }
//
// ToggleRow {
// checked: Config.bar.tray.showBluetooth
// text: qsTr("Bluetooth")
//
// onToggled: Config.bar.tray.showBluetooth = checked
// }
ToggleRow {
checked: Config.bar.tray.showPower
last: true
settingAnchor: "bar-status-power"
text: qsTr("Battery")
onToggled: Config.bar.tray.showPower = checked
}
// Behaviour
// Behavior
SectionHeader {
text: qsTr("Behavior")
}
ToggleRow {
checked: Config.bar.popouts.audio
checked: Config.bar.popouts.statusIcons
first: true
settingAnchor: "bar-status-audio-popout"
subtext: qsTr("Show a details popout when hovering the audio icons")
text: qsTr("Audio popout on hover")
settingAnchor: "bar-status-popout-on-hover"
subtext: qsTr("Show a details popout when hovering the status icons")
text: qsTr("Popout on hover")
onToggled: Config.bar.popouts.audio = checked
}
ToggleRow {
checked: Config.bar.popouts.upower
last: true
settingAnchor: "bar-status-power-popout"
subtext: qsTr("Show a details popout when hovering the power icon")
text: qsTr("Power popout on hover")
onToggled: Config.bar.popouts.upower = checked
onToggled: Config.bar.popouts.statusIcons = checked
}
}
}
@@ -28,8 +28,18 @@ PageBase {
onMoved: value => Config.bar.tray.trayIconSize = value
}
ToggleRow {
checked: Config.bar.popouts.tray
settingAnchor: "bar-tray-enable-tray-popouts"
subtext: Config.bar.popouts.tray ? qsTr("Enabled") : qsTr("Disabled")
text: qsTr("Enable tray menu popouts")
onToggled: Config.bar.popouts.tray = checked
}
ToggleRow {
checked: Config.bar.tray.showOnHover
enabled: Config.bar.popouts.tray
settingAnchor: "bar-tray-show-popout-on-hover"
subtext: Config.bar.tray.showOnHover ? qsTr("Will show context menu on hover") : qsTr("Will show context menu on right-click")
text: qsTr("Show popout on hover")