From d81808dd8f2a0044ded2f7a50fbba7d6636ab9ad Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 11 Aug 2026 14:05:57 +0200 Subject: [PATCH 1/2] refactor: use entries directly in bar object instead of nested components + various fixes and additions to settings --- Config/Appearance.qml | 120 ++++++- Config/AppearanceConf.qml | 81 ----- Config/BackgroundConfig.qml | 7 - Config/BarConfig.qml | 43 ++- Helpers/Time.qml | 14 +- Modules/Bar/Bar.qml | 135 ++++---- Modules/Settings/Common/ConnectedRect.qml | 6 + Modules/Settings/Common/DialogRowButton.qml | 249 ++++++++++++++ .../Settings/Common/DialogSelectButton.qml | 96 ++++++ Modules/Settings/Common/ListEditor.qml | 317 ++++++++++++++++++ Modules/Settings/Common/RowButton.qml | 93 +++++ Modules/Settings/Common/ToggleRow.qml | 12 + .../Pages/Panels/Bar/BarStatusIcons.qml | 132 ++++---- Modules/Settings/Pages/Panels/Bar/BarTray.qml | 10 + Modules/SysTray/StatusIcons.qml | 128 +++++++ Modules/SysTray/TrayIcons.qml | 53 +++ Modules/SysTray/TrayWidget.qml | 36 +- Modules/SysTray/Widgets/AudioWidget.qml | 75 +---- Modules/SysTray/Widgets/MicWidget.qml | 31 ++ Modules/SysTray/Widgets/UPowerWidget.qml | 7 - 20 files changed, 1321 insertions(+), 324 deletions(-) create mode 100644 Modules/Settings/Common/DialogRowButton.qml create mode 100644 Modules/Settings/Common/DialogSelectButton.qml create mode 100644 Modules/Settings/Common/ListEditor.qml create mode 100644 Modules/Settings/Common/RowButton.qml create mode 100644 Modules/SysTray/StatusIcons.qml create mode 100644 Modules/SysTray/TrayIcons.qml create mode 100644 Modules/SysTray/Widgets/MicWidget.qml diff --git a/Config/Appearance.qml b/Config/Appearance.qml index 7be277e..ab5120f 100644 --- a/Config/Appearance.qml +++ b/Config/Appearance.qml @@ -1,13 +1,119 @@ pragma Singleton import Quickshell +import QtQuick Singleton { - readonly property AppearanceConf.Anim anim: Config.appearance.anim - readonly property AppearanceConf.Deform deform: Config.appearance.deform - readonly property AppearanceConf.FontStuff font: Config.appearance.font - readonly property AppearanceConf.Padding padding: Config.appearance.padding - readonly property AppearanceConf.Rounding rounding: Config.appearance.rounding - readonly property AppearanceConf.Spacing spacing: Config.appearance.spacing - readonly property AppearanceConf.Transparency transparency: Config.appearance.transparency + property Anim anim: Anim { + } + property Deform deform: Deform { + } + property FontStuff font: FontStuff { + } + property Padding padding: Padding { + } + property Rounding rounding: Rounding { + } + property Spacing spacing: Spacing { + } + property Transparency transparency: Transparency { + } + + component Anim: QtObject { + property AnimCurves curves: AnimCurves { + } + property AnimDurations durations: AnimDurations { + } + property real mediaGifSpeedAdjustment: Config.appearance.anim.mediaGifSpeedAdjustment + property real sessionGifSpeed: Config.appearance.anim.sessionGifSpeed + } + component AnimCurves: QtObject { + property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] + property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] + property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] + property list expressiveDefaultEffects: [0.34, 0.8, 0.34, 1, 1, 1] + property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] + property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] + property list expressiveFastEffects: [0.31, 0.94, 0.34, 1, 1, 1] + property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] + property list expressiveSlowEffects: [0.34, 0.88, 0.34, 1, 1, 1] + property list expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] + property list standard: [0.2, 0, 0, 1, 1, 1] + property list standardAccel: [0.3, 0, 1, 1, 1, 1] + property list standardDecel: [0, 0, 0, 1, 1, 1] + } + component AnimDurations: QtObject { + property int expressiveDefaultSpatial: 500 * scale + property int expressiveEffects: 200 * scale + property int expressiveFastEffects: 150 * scale + property int expressiveFastSpatial: 350 * scale + property int expressiveSlowEffects: 300 * scale + property int extraLarge: 1000 * scale + property int large: 600 * scale + property int normal: 400 * scale + property real scale: 1 + property int small: 200 * scale + } + component Deform: QtObject { + property real scale: Config.appearance.deform.scale + } + component FontFamily: QtObject { + property string clock: Config.appearance.font.family.clock + property string material: Config.appearance.font.family.material + property string mono: Config.appearance.font.family.mono + property string sans: Config.appearance.font.family.sans + } + component FontSize: QtObject { + property int extraLarge: 28 * scale + property int large: 18 * scale + property int larger: 16 * scale + property int medium: 14 * scale + property int normal: 13 * scale + property real scale: 1 + property int small: 11 * scale + property int smaller: 12 * scale + } + component FontStuff: QtObject { + property FontFamily family: FontFamily { + } + property FontSize size: FontSize { + } + } + component Padding: QtObject { + property int extraLarge: 28 * scale + property int extraLargeIncreased: 32 * scale + property int extraSmall: 4 * scale + property int large: 16 * scale + property int largeIncreased: 20 * scale + property int larger: 12 * scale + property int normal: 8 * scale + property real scale: 1 + property int small: 5 * scale + property int smaller: 7 * scale + property int smallest: 2 * scale + } + component Rounding: QtObject { + property int extraSmall: 4 * scale + property int full: 1000 * scale + property int large: 24 * scale + property int medium: 16 * scale + property int normal: 18 * scale + property real scale: 1 + property int small: 12 * scale + property int smallest: 8 * scale + } + component Spacing: QtObject { + property int extraSmall: 4 * scale + property int large: 20 * scale + property int larger: 16 * scale + property int normal: 12 * scale + property real scale: 1 + property int small: 8 * scale + property int smaller: 10 * scale + } + component Transparency: QtObject { + property real base: Config.appearance.transparency.base + property bool enabled: Config.appearance.transparency.enabled + property real layers: Config.appearance.transparency.layers + } } diff --git a/Config/AppearanceConf.qml b/Config/AppearanceConf.qml index e2f3d07..8e76a58 100644 --- a/Config/AppearanceConf.qml +++ b/Config/AppearanceConf.qml @@ -7,50 +7,13 @@ JsonObject { } property FontStuff font: FontStuff { } - property Padding padding: Padding { - } - property Rounding rounding: Rounding { - } - property Spacing spacing: Spacing { - } property Transparency transparency: Transparency { } component Anim: JsonObject { - property AnimCurves curves: AnimCurves { - } - property AnimDurations durations: AnimDurations { - } property real mediaGifSpeedAdjustment: 300 property real sessionGifSpeed: 0.7 } - component AnimCurves: JsonObject { - property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] - property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] - property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] - property list expressiveDefaultEffects: [0.34, 0.8, 0.34, 1, 1, 1] - property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] - property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] - property list expressiveFastEffects: [0.31, 0.94, 0.34, 1, 1, 1] - property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] - property list expressiveSlowEffects: [0.34, 0.88, 0.34, 1, 1, 1] - property list expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] - property list standard: [0.2, 0, 0, 1, 1, 1] - property list standardAccel: [0.3, 0, 1, 1, 1, 1] - property list standardDecel: [0, 0, 0, 1, 1, 1] - } - component AnimDurations: JsonObject { - property int expressiveDefaultSpatial: 500 * scale - property int expressiveEffects: 200 * scale - property int expressiveFastEffects: 150 * scale - property int expressiveFastSpatial: 350 * scale - property int expressiveSlowEffects: 300 * scale - property int extraLarge: 1000 * scale - property int large: 600 * scale - property int normal: 400 * scale - property real scale: 1 - property int small: 200 * scale - } component Deform: JsonObject { property real scale: 1 } @@ -60,53 +23,9 @@ JsonObject { property string mono: "CaskaydiaCove NF" property string sans: "Segoe UI Variable Text" } - component FontSize: JsonObject { - property int extraLarge: 28 * scale - property int large: 18 * scale - property int larger: 16 * scale - property int medium: 14 * scale - property int normal: 13 * scale - property real scale: 1 - property int small: 11 * scale - property int smaller: 12 * scale - } component FontStuff: JsonObject { property FontFamily family: FontFamily { } - property FontSize size: FontSize { - } - } - component Padding: JsonObject { - property int extraLarge: 28 * scale - property int extraLargeIncreased: 32 * scale - property int extraSmall: 4 * scale - property int large: 16 * scale - property int largeIncreased: 20 * scale - property int larger: 12 * scale - property int normal: 8 * scale - property real scale: 1 - property int small: 5 * scale - property int smaller: 7 * scale - property int smallest: 2 * scale - } - component Rounding: JsonObject { - property int extraSmall: 4 * scale - property int full: 1000 * scale - property int large: 24 * scale - property int medium: 16 * scale - property int normal: 18 * scale - property real scale: 1 - property int small: 12 * scale - property int smallest: 8 * scale - } - component Spacing: JsonObject { - property int extraSmall: 4 * scale - property int large: 20 * scale - property int larger: 16 * scale - property int normal: 12 * scale - property real scale: 1 - property int small: 8 * scale - property int smaller: 10 * scale } component Transparency: JsonObject { property real base: 0.85 diff --git a/Config/BackgroundConfig.qml b/Config/BackgroundConfig.qml index 10e7691..bcae021 100644 --- a/Config/BackgroundConfig.qml +++ b/Config/BackgroundConfig.qml @@ -4,11 +4,4 @@ import qs.Config JsonObject { property bool enabled: true property int wallFadeDuration: MaterialEasing.standardTime - property real alignX: 0.5 - property real alignY: 0.5 - property real zoom: 1.0 - property real sourceClipX: 0 - property real sourceClipY: 0 - property real sourceClipW: 0 - property real sourceClipH: 0 } diff --git a/Config/BarConfig.qml b/Config/BarConfig.qml index 1219a17..4f3697f 100644 --- a/Config/BarConfig.qml +++ b/Config/BarConfig.qml @@ -40,6 +40,10 @@ JsonObject { id: "tray", enabled: true }, + { + id: "statusIcons", + enabled: true + }, { id: "network", enabled: false @@ -63,23 +67,38 @@ JsonObject { } component Popouts: JsonObject { - property bool activeWindow: true - property bool audio: true - property bool clock: true - property bool network: true - property bool resources: true + property bool statusIcons: true property bool tray: true - property bool upower: true } component Tray: JsonObject { property bool recolorIcons: false - property bool showAudio: true - property bool showBluetooth: false - property bool showMicrophone: true - property bool showNetwork: false property bool showOnHover: false - property bool showPower: true - property bool showWifi: false + property list statusIcons: [ + { + id: "audio", + enabled: true + }, + { + id: "microphone", + enabled: true + }, + { + id: "bluetooth", + enabled: true + }, + { + id: "network", + enabled: false + }, + { + id: "wifi", + enabled: false + }, + { + id: "power", + enabled: true + }, + ] property int trayIconSize: 24 } } diff --git a/Helpers/Time.qml b/Helpers/Time.qml index f7d75f1..778af40 100644 --- a/Helpers/Time.qml +++ b/Helpers/Time.qml @@ -20,7 +20,19 @@ Singleton { readonly property string timeStr: format("hh:mm:ss") function format(fmt: string): string { - return Qt.formatDateTime(clock.date, fmt); + return Qt.formatDateTime(clock.date, resolveFormat(fmt)); + } + + function resolveFormat(fmt: string): string { + if (!Config.services.useTwelveHourClock) + return fmt; + + let f = fmt.replace(/hh/g, "h"); + + if (/h/.test(f) && !/AP|ap/.test(f)) + f += " AP"; + + return f; } SystemClock { diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index ae4374e..b2a1842 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -12,17 +12,18 @@ import qs.Modules.Updates RowLayout { id: root + required property bool fullscreen required property Wrapper popouts required property ClipWrapper popoutsWrapper required property ShellScreen screen - required property bool fullscreen readonly property int vPadding: 6 required property PersistentProperties visibilities function checkPopout(x: real): void { - const ch = childAt(x, height / 2) as WrappedLoader; + const ch = childAt(x, height / 2) as EntryWrapper; + const id = ch?.entryId; - if (!ch || ch?.id === "spacer") { + if (!ch || id === undefined) { if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) popouts.hasCurrent = false; return; @@ -31,32 +32,38 @@ RowLayout { if (visibilities.sidebar || visibilities.dashboard || visibilities.resources || visibilities.settings) return; - if (ch.id === "tray") { - const tray = ch.item; - const localPos = tray.mapFromItem(root, x, height / 2); - const sub = tray.getHoveredSubItem(localPos.x, localPos.y); - if (sub) { - popouts.currentName = sub.id; + if (id === "statusIcons" && Config.bar.popouts.statusIcons) { + const items = (ch.item as StatusIcons).items; + const localX = mapToItem(items, x, 0).x; + const icon = items.childAt(localX, items.height / 2); + if (icon) { + popouts.currentName = icon.name; + popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, icon.implicitWidth / 2, 0).x); + popouts.hasCurrent = true; + } + } else if (id === "tray" && Config.bar.popouts.tray && Config.bar.tray.showOnHover) { + const tray = ch.item as TrayIcons; + const layout = tray.layout; + const localX = mapToItem(layout, x, 0).x; + const trayItem = layout.childAt(localX, layout.height / 2); + + if (trayItem) { + const idx = trayItem.index; + popouts.currentName = `traymenu${idx}`; popouts.currentCenter = Qt.binding(() => { - const centerX = sub.item.mapToItem(root, sub.item.width / 2, 0).x; - return centerX; + const it = tray.items.itemAt(idx); + return it ? it.mapToItem(root, it.implicitWidth / 2, 0).x : popouts.currentCenter; }); popouts.hasCurrent = true; - return; } - if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) + if (!popouts.currentName.startsWith("traymenu")) popouts.hasCurrent = false; } - const id = ch.id; - const top = ch.x; - const item = ch.item; - const itemWidth = item.implicitWidth; - if (id === "updates") { popouts.currentName = "updates"; - popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); + popouts.currentCenter = Qt.binding(() => ch.item.mapToItem(root, (ch.item as Item).implicitWidth / 2, 0).x); popouts.hasCurrent = true; } } @@ -66,7 +73,9 @@ RowLayout { Repeater { id: repeater - model: Config.bar.entries + model: ScriptModel { + values: Config.bar.entries.filter(e => e.enabled) + } DelegateChooser { role: "id" @@ -74,8 +83,8 @@ RowLayout { DelegateChoice { roleValue: "hyprsunset" - delegate: WrappedLoader { - sourceComponent: HyprsunsetWidget { + delegate: EntryWrapper { + HyprsunsetWidget { visible: !root.fullscreen } } @@ -84,7 +93,7 @@ RowLayout { DelegateChoice { roleValue: "spacer" - delegate: WrappedLoader { + delegate: EntryWrapper { Layout.fillWidth: true } } @@ -92,8 +101,8 @@ RowLayout { DelegateChoice { roleValue: "workspaces" - delegate: WrappedLoader { - sourceComponent: Workspaces { + delegate: EntryWrapper { + Workspaces { screen: root.screen visible: !root.fullscreen } @@ -103,8 +112,8 @@ RowLayout { DelegateChoice { roleValue: "tray" - delegate: WrappedLoader { - sourceComponent: TrayWidget { + delegate: EntryWrapper { + TrayIcons { loader: root popouts: root.popouts visible: !root.fullscreen @@ -112,11 +121,20 @@ RowLayout { } } + DelegateChoice { + roleValue: "statusIcons" + + delegate: EntryWrapper { + StatusIcons { + } + } + } + DelegateChoice { roleValue: "resources" - delegate: WrappedLoader { - sourceComponent: Resources { + delegate: EntryWrapper { + Resources { visibilities: root.visibilities visible: !root.fullscreen } @@ -126,8 +144,8 @@ RowLayout { DelegateChoice { roleValue: "updates" - delegate: WrappedLoader { - sourceComponent: UpdatesWidget { + delegate: EntryWrapper { + UpdatesWidget { visible: !root.fullscreen } } @@ -136,8 +154,8 @@ RowLayout { DelegateChoice { roleValue: "notifBell" - delegate: WrappedLoader { - sourceComponent: NotifBell { + delegate: EntryWrapper { + NotifBell { popouts: root.popouts visibilities: root.visibilities visible: !root.fullscreen @@ -148,8 +166,8 @@ RowLayout { DelegateChoice { roleValue: "clock" - delegate: WrappedLoader { - sourceComponent: Clock { + delegate: EntryWrapper { + Clock { loader: root popouts: root.popouts visibilities: root.visibilities @@ -161,8 +179,8 @@ RowLayout { DelegateChoice { roleValue: "activeWindow" - delegate: WrappedLoader { - sourceComponent: WindowTitle { + delegate: EntryWrapper { + WindowTitle { bar: root visible: !root.fullscreen } @@ -172,8 +190,8 @@ RowLayout { DelegateChoice { roleValue: "network" - delegate: WrappedLoader { - sourceComponent: NetworkWidget { + delegate: EntryWrapper { + NetworkWidget { } } } @@ -181,8 +199,8 @@ RowLayout { DelegateChoice { roleValue: "media" - delegate: WrappedLoader { - sourceComponent: MediaWidget { + delegate: EntryWrapper { + MediaWidget { visible: !root.fullscreen } } @@ -190,34 +208,17 @@ RowLayout { } } - component WrappedLoader: Loader { - required property bool enabled - required property string id + component EntryWrapper: Item { + readonly property string entryId: modelData.id required property int index - - function findFirstEnabled(): Item { - const count = repeater.count; - for (let i = 0; i < count; i++) { - const item = repeater.itemAt(i); - if (item?.enabled) - return item; - } - return null; - } - - function findLastEnabled(): Item { - for (let i = repeater.count - 1; i >= 0; i--) { - const item = repeater.itemAt(i); - if (item?.enabled) - return item; - } - return null; - } + default property Item item + required property var modelData Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: findFirstEnabled() === this ? root.vPadding : 0 - Layout.rightMargin: findLastEnabled() === this ? root.vPadding : 0 - active: enabled - visible: enabled + Layout.leftMargin: index === 0 ? root.vPadding : (entryId === "statusIcons") ? -root.spacing + Appearance.spacing.extraSmall : 0 + Layout.rightMargin: index === repeater.count - 1 ? root.vPadding : 0 + children: item + implicitHeight: item?.implicitHeight ?? 0 + implicitWidth: item?.implicitWidth ?? 0 } } diff --git a/Modules/Settings/Common/ConnectedRect.qml b/Modules/Settings/Common/ConnectedRect.qml index 8bb9829..baf944c 100644 --- a/Modules/Settings/Common/ConnectedRect.qml +++ b/Modules/Settings/Common/ConnectedRect.qml @@ -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 diff --git a/Modules/Settings/Common/DialogRowButton.qml b/Modules/Settings/Common/DialogRowButton.qml new file mode 100644 index 0000000..30a53c3 --- /dev/null +++ b/Modules/Settings/Common/DialogRowButton.qml @@ -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 + } + } + } +} diff --git a/Modules/Settings/Common/DialogSelectButton.qml b/Modules/Settings/Common/DialogSelectButton.qml new file mode 100644 index 0000000..59b684c --- /dev/null +++ b/Modules/Settings/Common/DialogSelectButton.qml @@ -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; + } +} diff --git a/Modules/Settings/Common/ListEditor.qml b/Modules/Settings/Common/ListEditor.qml new file mode 100644 index 0000000..2548d9e --- /dev/null +++ b/Modules/Settings/Common/ListEditor.qml @@ -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) + } + } + } + } +} diff --git a/Modules/Settings/Common/RowButton.qml b/Modules/Settings/Common/RowButton.qml new file mode 100644 index 0000000..48f2aab --- /dev/null +++ b/Modules/Settings/Common/RowButton.qml @@ -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 + } + } + } +} diff --git a/Modules/Settings/Common/ToggleRow.qml b/Modules/Settings/Common/ToggleRow.qml index 77dd3e8..00b437e 100644 --- a/Modules/Settings/Common/ToggleRow.qml +++ b/Modules/Settings/Common/ToggleRow.qml @@ -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 { + } + } } } } diff --git a/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml b/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml index b2655d5..ca4e5ca 100644 --- a/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml +++ b/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml @@ -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 } } } diff --git a/Modules/Settings/Pages/Panels/Bar/BarTray.qml b/Modules/Settings/Pages/Panels/Bar/BarTray.qml index 023640d..260cfcf 100644 --- a/Modules/Settings/Pages/Panels/Bar/BarTray.qml +++ b/Modules/Settings/Pages/Panels/Bar/BarTray.qml @@ -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") diff --git a/Modules/SysTray/StatusIcons.qml b/Modules/SysTray/StatusIcons.qml new file mode 100644 index 0000000..5e40cdb --- /dev/null +++ b/Modules/SysTray/StatusIcons.qml @@ -0,0 +1,128 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import QtQuick.Layouts +import qs.Helpers +import qs.Modules.SysTray.Widgets +import qs.Config +import qs.Components + +CustomClippingRect { + id: root + + readonly property int firstPresent: { + const values = model.values; + for (let i = 0; i < values.length; i++) + if (!collapsed(values[i])) + return i; + return -1; + } + + // Index of the first/last entry that isn't collapsed, for edge margin gating + readonly property alias items: row + readonly property int lastPresent: { + const values = model.values; + for (let i = values.length - 1; i >= 0; i--) + if (!collapsed(values[i])) + return i; + return -1; + } + readonly property int spacing: Appearance.spacing.normal / 2 + + // Entries that can shrink to nothing, spacing included + function collapsed(entry: var): bool { + if (entry.id === "lockStatus") + return !Hypr.capsLock && !Hypr.numLock; + return false; + } + + bottomLeftRadius: Appearance.rounding.smallest / 2 + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.bar.height + Appearance.padding.smallest * 2 + implicitWidth: row.implicitWidth + Appearance.padding.small * 2 + radius: Appearance.rounding.full + topLeftRadius: Appearance.rounding.smallest / 2 + + RowLayout { + id: row + + anchors.fill: parent + anchors.leftMargin: Appearance.padding.small + anchors.rightMargin: Appearance.padding.small + + Repeater { + model: ScriptModel { + id: model + + values: Config.bar.tray.statusIcons.filter(e => e.enabled) + } + + DelegateChooser { + role: "id" + + DelegateChoice { + roleValue: "audio" + + delegate: EntryWrapper { + name: "audio" + + AudioWidget { + } + } + } + + DelegateChoice { + roleValue: "microphone" + + delegate: EntryWrapper { + name: "audio" + + MicWidget { + } + } + } + + DelegateChoice { + roleValue: "power" + + delegate: EntryWrapper { + name: "upower" + + UPowerWidget { + } + } + } + } + } + } + + component EntryWrapper: Item { + required property int index + default property Item item + property real leftGap: present && index !== root.lastPresent ? margin : 0 + property int margin: root.spacing / 2 + required property var modelData + property string name: modelData.id.toLowerCase() + readonly property bool present: !root.collapsed(modelData) + property real rightGap: present && index !== root.firstPresent ? margin : 0 + + Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: Math.round(leftGap) + Layout.rightMargin: Math.round(rightGap) + children: item + implicitHeight: item?.implicitHeight ?? 0 + implicitWidth: item?.implicitWidth ?? 0 + + Behavior on leftGap { + Anim { + type: Anim.SlowEffects + } + } + Behavior on rightGap { + Anim { + type: Anim.SlowEffects + } + } + } +} diff --git a/Modules/SysTray/TrayIcons.qml b/Modules/SysTray/TrayIcons.qml new file mode 100644 index 0000000..7d47919 --- /dev/null +++ b/Modules/SysTray/TrayIcons.qml @@ -0,0 +1,53 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.SystemTray +import qs.Components +import qs.Config +import qs.Modules.SysTray.Widgets +import qs.Modules + +CustomClippingRect { + id: root + + readonly property alias items: repeater + readonly property alias layout: sysRow + required property RowLayout loader + readonly property int padding: Appearance.padding.small + required property Wrapper popouts + + bottomRightRadius: Appearance.rounding.smallest / 2 + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.bar.height + Appearance.padding.smallest * 2 + implicitWidth: sysRow.implicitWidth + Appearance.padding.small * 2 + radius: Appearance.rounding.full + topRightRadius: Appearance.rounding.smallest / 2 + + RowLayout { + id: sysRow + + anchors.centerIn: parent + spacing: 0 + + Repeater { + id: repeater + + model: SystemTray.items + + TrayItem { + id: trayItem + + required property int index + required property SystemTrayItem modelData + + implicitHeight: 34 + implicitWidth: 34 + ind: index + item: modelData + loader: root.loader + popouts: root.popouts + } + } + } +} diff --git a/Modules/SysTray/TrayWidget.qml b/Modules/SysTray/TrayWidget.qml index f4be726..3797200 100644 --- a/Modules/SysTray/TrayWidget.qml +++ b/Modules/SysTray/TrayWidget.qml @@ -44,15 +44,20 @@ RowLayout { function getHoveredSubItem(localX, localY) { let modPos = mapToItem(sysTrayMod, localX, localY); if (sysTrayMod.contains(Qt.point(modPos.x, modPos.y))) { - let modRowPos = sysTrayMod.mapToItem(sysModRow, modPos.x, modPos.y); - let child = closestRowChild(sysModRow, modRowPos.x); + let modRowPos = sysTrayMod.mapToItem(sysIcons, modPos.x, modPos.y); + let child = closestRowChild(sysIcons, modRowPos.x); if (child) { - if (child.objectName === "audioWidget" && Config.bar.popouts.audio) + if (child.objectName === "audio" && Config.bar.popouts.audio) return { id: "audio", item: child }; - if (child.objectName === "upowerWidget" && Config.bar.popouts.upower) + if (child.objectName === "microphone" && Config.bar.popouts.audio) + return { + id: "audio", + item: child + }; + if (child.objectName === "upower" && Config.bar.popouts.upower) return { id: "upower", item: child @@ -123,33 +128,16 @@ RowLayout { Layout.fillHeight: true bottomLeftRadius: Appearance.rounding.smallest / 2 color: DynamicColors.tPalette.m3surfaceContainer - implicitWidth: sysModRow.implicitWidth + Appearance.padding.smaller + Appearance.padding.normal + implicitWidth: sysIcons.implicitWidth + Appearance.padding.smaller + Appearance.padding.normal radius: Appearance.rounding.full topLeftRadius: Appearance.rounding.smallest / 2 - RowLayout { - id: sysModRow + StatusIcons { + id: sysIcons anchors.fill: parent anchors.leftMargin: Appearance.padding.smaller anchors.rightMargin: Appearance.padding.normal - - AudioWidget { - Layout.fillHeight: true - Layout.rightMargin: hasContent ? Appearance.spacing.extraSmall / 2 : 0 - objectName: "audioWidget" - - Behavior on Layout.rightMargin { - Anim { - type: Anim.FastEffects - } - } - } - - UPowerWidget { - Layout.fillHeight: true - objectName: "upowerWidget" - } } } } diff --git a/Modules/SysTray/Widgets/AudioWidget.qml b/Modules/SysTray/Widgets/AudioWidget.qml index 3ecac22..4555d11 100644 --- a/Modules/SysTray/Widgets/AudioWidget.qml +++ b/Modules/SysTray/Widgets/AudioWidget.qml @@ -7,72 +7,25 @@ import qs.Modules import qs.Config import qs.Components -RowLayout { - id: root +MaterialIcon { + id: speaker - property color barColor: DynamicColors.palette.m3primary - readonly property bool hasContent: mic.visible || speaker.visible - property color textColor: DynamicColors.palette.m3onSurface + animate: true + color: Audio.muted ? DynamicColors.palette.m3error : DynamicColors.palette.m3onSurface + fill: 1 + font.pointSize: Appearance.font.size.larger + text: Audio.muted ? "volume_off" : "volume_up" - width: hasContent ? implicitWidth : 0 - - MaterialIcon { - id: mic - - readonly property bool shouldBeVisible: Config.bar.tray.showMicrophone - - Layout.alignment: Qt.AlignVCenter - Layout.maximumWidth: shouldBeVisible ? implicitWidth : 0 - animate: true - color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor - fill: 1 - font.pointSize: Appearance.font.size.larger - opacity: shouldBeVisible ? 1 : 0 - scale: shouldBeVisible ? 1 : 0 - text: Audio.sourceMuted ? "mic_off" : "mic" - visible: opacity > 0 - - Behavior on Layout.maximumWidth { - Anim { - } - } - Behavior on opacity { - Anim { - } - } - Behavior on scale { - Anim { - } + Behavior on Layout.maximumWidth { + Anim { } } - - MaterialIcon { - id: speaker - - readonly property bool shouldBeVisible: Config.bar.tray.showAudio - - Layout.alignment: Qt.AlignVCenter - Layout.maximumWidth: shouldBeVisible ? implicitWidth : 0 - animate: true - color: Audio.muted ? DynamicColors.palette.m3error : root.textColor - fill: 1 - font.pointSize: Appearance.font.size.larger - opacity: shouldBeVisible ? 1 : 0 - scale: shouldBeVisible ? 1 : 0 - text: Audio.muted ? "volume_off" : "volume_up" - visible: opacity > 0 - - Behavior on Layout.maximumWidth { - Anim { - } + Behavior on opacity { + Anim { } - Behavior on opacity { - Anim { - } - } - Behavior on scale { - Anim { - } + } + Behavior on scale { + Anim { } } } diff --git a/Modules/SysTray/Widgets/MicWidget.qml b/Modules/SysTray/Widgets/MicWidget.qml new file mode 100644 index 0000000..2aa51ce --- /dev/null +++ b/Modules/SysTray/Widgets/MicWidget.qml @@ -0,0 +1,31 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Io +import Quickshell.Services.Pipewire +import qs.Daemons +import qs.Modules +import qs.Config +import qs.Components + +MaterialIcon { + id: mic + + animate: true + color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : DynamicColors.palette.m3onSurface + fill: 1 + font.pointSize: Appearance.font.size.larger + text: Audio.sourceMuted ? "mic_off" : "mic" + + Behavior on Layout.maximumWidth { + Anim { + } + } + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } +} diff --git a/Modules/SysTray/Widgets/UPowerWidget.qml b/Modules/SysTray/Widgets/UPowerWidget.qml index 0c4b4c8..6968b4f 100644 --- a/Modules/SysTray/Widgets/UPowerWidget.qml +++ b/Modules/SysTray/Widgets/UPowerWidget.qml @@ -8,15 +8,8 @@ import qs.Helpers Item { id: root - readonly property bool shouldBeActive: Config.bar.tray.showPower - - Layout.preferredHeight: shouldBeActive ? implicitHeight : 0 - Layout.preferredWidth: shouldBeActive ? implicitWidth : 0 implicitHeight: Battery.isLaptop ? batteryIconLoader.item.implicitHeight : upowerIconLoader.item.implicitHeight implicitWidth: Battery.isLaptop ? batteryIconLoader.item.implicitWidth : upowerIconLoader.item.implicitWidth - opacity: shouldBeActive ? 1 : 0 - scale: shouldBeActive ? 1 : 0 - visible: opacity > 0 Behavior on Layout.preferredHeight { Anim { -- 2.47.3 From 2e4517b0a6eef707a98bfe4b127ccc981a7e7d71 Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 11 Aug 2026 15:00:44 +0200 Subject: [PATCH 2/2] simpler model for status entries --- Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml b/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml index ca4e5ca..e8fd036 100644 --- a/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml +++ b/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml @@ -42,7 +42,7 @@ PageBase { } first: true - values: Config.bar.tray.statusIcons.map(item => JSON.parse(JSON.stringify(item))) + values: Config.bar.tray.statusIcons z: 1 onItemMoved: (from, to) => { -- 2.47.3