summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGregor Kleen <gkleen@yggdrasil.li>2025-09-11 14:43:11 +0200
committerGregor Kleen <gkleen@yggdrasil.li>2025-09-11 14:43:11 +0200
commitaebd3235d755cb1ff95995b461e497fea2d52e8b (patch)
treea8b2cc30a51ed06c383d7a6f53cb44f623e188c1
parentd1261f6586ab5d91e81c04ca0f586e35ee6fb8a6 (diff)
downloadnixos-aebd3235d755cb1ff95995b461e497fea2d52e8b.tar
nixos-aebd3235d755cb1ff95995b461e497fea2d52e8b.tar.gz
nixos-aebd3235d755cb1ff95995b461e497fea2d52e8b.tar.bz2
nixos-aebd3235d755cb1ff95995b461e497fea2d52e8b.tar.xz
nixos-aebd3235d755cb1ff95995b461e497fea2d52e8b.zip
-rw-r--r--accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml182
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Bar.qml12
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml71
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml47
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Clock.qml6
-rw-r--r--accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml412
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml13
-rw-r--r--accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml4
8 files changed, 527 insertions, 220 deletions
diff --git a/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
index 57ade488..883f9001 100644
--- a/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
+++ b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
@@ -22,72 +22,144 @@ Item {
22 width: activeWindowDisplayContent.width 22 width: activeWindowDisplayContent.width
23 height: parent.height 23 height: parent.height
24 24
25 Row { 25 WrapperMouseArea {
26 id: activeWindowDisplayContent 26 id: widgetMouseArea
27 27
28 width: childrenRect.width 28 anchors.fill: parent
29 height: parent.height
30 anchors.verticalCenter: parent.verticalCenter
31 spacing: 8
32 29
33 IconImage { 30 hoverEnabled: true
34 id: activeWindowIcon
35 31
36 height: 14 32 Item {
37 width: 14 33 anchors.fill: parent
38 34
39 anchors.verticalCenter: parent.verticalCenter 35 Row {
36 id: activeWindowDisplayContent
40 37
41 source: { 38 width: childrenRect.width
42 let icon = activeWindowDisplay.windowEntry?.icon 39 height: parent.height
43 if (typeof icon === 'string' || icon instanceof String) { 40 anchors.verticalCenter: parent.verticalCenter
44 if (icon.includes("?path=")) { 41 spacing: 8
45 const split = icon.split("?path=") 42
46 if (split.length !== 2) 43 IconImage {
44 id: activeWindowIcon
45
46 implicitSize: 14
47
48 anchors.verticalCenter: parent.verticalCenter
49
50 source: {
51 let icon = activeWindowDisplay.windowEntry?.icon
52 if (typeof icon === 'string' || icon instanceof String) {
53 if (icon.includes("?path=")) {
54 const split = icon.split("?path=")
55 if (split.length !== 2)
56 return icon
57 const name = split[0]
58 const path = split[1]
59 const fileName = name.substring(
60 name.lastIndexOf("/") + 1)
61 return `file://${path}/${fileName}`
62 } else
63 icon = Quickshell.iconPath(icon);
47 return icon 64 return icon
48 const name = split[0] 65 }
49 const path = split[1] 66 return ""
50 const fileName = name.substring( 67 }
51 name.lastIndexOf("/") + 1) 68 asynchronous: true
52 return `file://${path}/${fileName}` 69 smooth: true
53 } else 70 mipmap: true
54 icon = Quickshell.iconPath(icon); 71 }
55 return icon 72
73 Text {
74 id: windowTitle
75
76 width: Math.min(implicitWidth, activeWindowDisplay.maxWidth - activeWindowIcon.width - activeWindowDisplayContent.spacing)
77
78 property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" }
79
80 elide: Text.ElideRight
81 maximumLineCount: 1
82 color: "white"
83 anchors.verticalCenter: parent.verticalCenter
84 text: {
85 if (!activeWindowDisplay.activeWindow)
86 return "";
87
88 var title = activeWindowDisplay.activeWindow.title;
89 var appName = activeWindowDisplay.windowEntry?.name;
90 if (appAliases[appName])
91 appName = appAliases[appName];
92 if (appName && title.endsWith(appName)) {
93 const oldTitle = title;
94 title = title.substring(0, title.length - appName.length);
95 title = title.replace(/\s*(—|-)\s*$/, "");
96 if (!title)
97 title = oldTitle;
98 }
99 return title;
100 }
56 } 101 }
57 return ""
58 } 102 }
59 asynchronous: true
60 smooth: true
61 mipmap: true
62 } 103 }
104 }
105
106 Loader {
107 id: tooltipLoader
108
109 active: false
110
111 Connections {
112 target: widgetMouseArea
113 function onContainsMouseChanged() {
114 if (widgetMouseArea.containsMouse)
115 tooltipLoader.active = true;
116 }
117 }
118
119 PopupWindow {
120 id: tooltip
121
122 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse
123
124 anchor {
125 item: widgetMouseArea
126 edges: Edges.Bottom | Edges.Left
127 }
128 visible: false
129
130 onNextVisibleChanged: hangTimer.restart()
131
132 Timer {
133 id: hangTimer
134 interval: tooltip.visible ? 100 : 500
135 onTriggered: {
136 tooltip.visible = tooltip.nextVisible;
137 if (!tooltip.visible)
138 tooltipLoader.active = false;
139 }
140 }
141
142 implicitWidth: widgetTooltipText.contentWidth + 16
143 implicitHeight: widgetTooltipText.contentHeight + 16
144 color: "black"
145
146 WrapperMouseArea {
147 id: tooltipMouseArea
148
149 hoverEnabled: true
150 enabled: true
151
152 anchors.centerIn: parent
153
154 Text {
155 id: widgetTooltipText
156
157 font.pointSize: 10
158 font.family: "Fira Mono"
159 color: "white"
63 160
64 Text { 161 text: JSON.stringify(Object.assign({}, activeWindowDisplay.activeWindow), null, 2)
65 id: windowTitle
66
67 width: Math.min(implicitWidth, activeWindowDisplay.maxWidth - activeWindowIcon.width - activeWindowDisplayContent.spacing)
68
69 property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" }
70
71 elide: Text.ElideRight
72 maximumLineCount: 1
73 color: "white"
74 anchors.verticalCenter: parent.verticalCenter
75 text: {
76 if (!activeWindowDisplay.activeWindow)
77 return "";
78
79 var title = activeWindowDisplay.activeWindow.title;
80 var appName = activeWindowDisplay.windowEntry?.name;
81 if (appAliases[appName])
82 appName = appAliases[appName];
83 if (appName && title.endsWith(appName)) {
84 const oldTitle = title;
85 title = title.substring(0, title.length - appName.length);
86 title = title.replace(/\s*(—|-)\s*$/, "");
87 if (!title)
88 title = oldTitle;
89 } 162 }
90 return title;
91 } 163 }
92 } 164 }
93 } 165 }
diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml
index d52740b2..52b9b344 100644
--- a/accounts/gkleen@sif/shell/quickshell/Bar.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml
@@ -70,23 +70,13 @@ PanelWindow {
70 Item { 70 Item {
71 enabled: privacy.active 71 enabled: privacy.active
72 height: parent.height 72 height: parent.height
73 width: 8 73 width: 8 - 4
74 } 74 }
75 75
76 BatteryWidget {} 76 BatteryWidget {}
77 77
78 Item {
79 height: parent.height
80 width: 8
81 }
82
83 BrightnessWidget {} 78 BrightnessWidget {}
84 79
85 Item {
86 height: parent.height
87 width: 4
88 }
89
90 PipewireWidget {} 80 PipewireWidget {}
91 81
92 SystemTray {} 82 SystemTray {}
diff --git a/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml b/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml
index 896440f1..fd031627 100644
--- a/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml
+++ b/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml
@@ -7,7 +7,7 @@ Item {
7 id: root 7 id: root
8 8
9 height: parent.height 9 height: parent.height
10 width: batteryIcon.width 10 width: batteryIcon.width + 8
11 anchors.verticalCenter: parent.verticalCenter 11 anchors.verticalCenter: parent.verticalCenter
12 12
13 property var batteryDevice: Array.from(UPower.devices.values).find(dev => dev.isLaptopBattery) 13 property var batteryDevice: Array.from(UPower.devices.values).find(dev => dev.isLaptopBattery)
@@ -55,7 +55,76 @@ Item {
55 return "#555"; 55 return "#555";
56 } 56 }
57 } 57 }
58 }
59 }
60
61 PopupWindow {
62 id: tooltip
63
64 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse
65
66 anchor {
67 item: widgetMouseArea
68 edges: Edges.Bottom | Edges.Left
69 }
70 visible: false
71
72 onNextVisibleChanged: hangTimer.restart()
73
74 Timer {
75 id: hangTimer
76 interval: 100
77 onTriggered: tooltip.visible = tooltip.nextVisible
78 }
79
80 implicitWidth: widgetTooltipText.contentWidth + 16
81 implicitHeight: widgetTooltipText.contentHeight + 16
82 color: "black"
58 83
84 WrapperMouseArea {
85 id: tooltipMouseArea
86
87 hoverEnabled: true
88 enabled: true
89
90 anchors.centerIn: parent
91
92 Text {
93 id: widgetTooltipText
94
95 font.pointSize: 10
96 font.family: "Fira Sans"
97 color: "white"
98
99 text: {
100 const stateStr = UPowerDeviceState.toString(root.batteryDevice.state);
101 var outStr = stateStr;
102 if (root.batteryDevice.state != UPowerDeviceState.FullyCharged)
103 outStr += ` ${Math.round(root.batteryDevice.percentage * 100)}%`;
104
105 function formatTime(t) {
106 var res = "";
107 for (const unit of [{ "s": "h", "v": 3600 }, { "s": "m", "v": 60 }, { "s": "s", "v": 1 }]) {
108 if (t < unit.v)
109 continue;
110 res += Math.floor(t / unit.v) + unit.s;
111 t %= unit.v;
112 }
113 return res;
114 }
115 if (root.batteryDevice.timeToEmpty != 0) {
116 const tStr = formatTime(Math.floor(root.batteryDevice.timeToEmpty / 60) * 60);
117 if (tStr)
118 outStr += " " + tStr;
119 } else if (root.batteryDevice.timeToFull != 0) {
120 const tStr = formatTime(Math.ceil(root.batteryDevice.timeToFull / 60) * 60);
121 if (tStr)
122 outStr += " " + tStr;
123 }
124
125 return outStr;
126 }
127 }
59 } 128 }
60 } 129 }
61} 130}
diff --git a/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml b/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml
index 664b28e2..7f9c1ad0 100644
--- a/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml
+++ b/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml
@@ -5,7 +5,7 @@ import qs.Services
5 5
6Item { 6Item {
7 height: parent.height 7 height: parent.height
8 width: brightnessIcon.width 8 width: brightnessIcon.width + 8
9 anchors.verticalCenter: parent.verticalCenter 9 anchors.verticalCenter: parent.verticalCenter
10 10
11 WrapperMouseArea { 11 WrapperMouseArea {
@@ -13,6 +13,8 @@ Item {
13 13
14 anchors.fill: parent 14 anchors.fill: parent
15 15
16 hoverEnabled: true
17
16 property real sensitivity: (1 / 50) / 120 18 property real sensitivity: (1 / 50) / 120
17 onWheel: event => Brightness.currBrightness += event.angleDelta.y * sensitivity 19 onWheel: event => Brightness.currBrightness += event.angleDelta.y * sensitivity
18 20
@@ -30,4 +32,47 @@ Item {
30 } 32 }
31 } 33 }
32 } 34 }
35
36 PopupWindow {
37 id: tooltip
38
39 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse
40
41 anchor {
42 item: widgetMouseArea
43 edges: Edges.Bottom | Edges.Left
44 }
45 visible: false
46
47 onNextVisibleChanged: hangTimer.restart()
48
49 Timer {
50 id: hangTimer
51 interval: 100
52 onTriggered: tooltip.visible = tooltip.nextVisible
53 }
54
55 implicitWidth: widgetTooltipText.contentWidth + 16
56 implicitHeight: widgetTooltipText.contentHeight + 16
57 color: "black"
58
59 WrapperMouseArea {
60 id: tooltipMouseArea
61
62 hoverEnabled: true
63 enabled: true
64
65 anchors.centerIn: parent
66
67 Text {
68 id: widgetTooltipText
69
70 font.pointSize: 10
71 font.family: "Fira Sans"
72 color: "white"
73
74 text: `${Math.round(Brightness.currBrightness * 100)}%`
75 }
76 }
77 }
33} 78}
diff --git a/accounts/gkleen@sif/shell/quickshell/Clock.qml b/accounts/gkleen@sif/shell/quickshell/Clock.qml
index 4644d5e7..bb618f6a 100644
--- a/accounts/gkleen@sif/shell/quickshell/Clock.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Clock.qml
@@ -73,7 +73,11 @@ Item {
73 Timer { 73 Timer {
74 id: hangTimer 74 id: hangTimer
75 interval: 100 75 interval: 100
76 onTriggered: tooltip.visible = tooltip.nextVisible 76 onTriggered: {
77 tooltip.visible = tooltip.nextVisible;
78 if (!tooltip.visible)
79 tooltipLoader.active = false;
80 }
77 } 81 }
78 82
79 implicitWidth: clockTooltipContent.width 83 implicitWidth: clockTooltipContent.width
diff --git a/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
index 007ce100..3e0b8fd9 100644
--- a/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
+++ b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
@@ -114,232 +114,350 @@ Item {
114 114
115 implicitWidth: tooltipContent.width 115 implicitWidth: tooltipContent.width
116 implicitHeight: tooltipContent.height 116 implicitHeight: tooltipContent.height
117 color: "black" 117 color: "transparent"
118 118
119 WrapperMouseArea { 119 Rectangle {
120 id: tooltipMouseArea 120 width: tooltip.width
121 height: tooltipLayout.childrenRect.height + 16
122 color: "black"
123 }
121 124
122 hoverEnabled: true 125 WrapperItem {
123 enabled: true 126 id: tooltipContent
124 127
125 anchors.fill: parent 128 bottomMargin: Math.max(0, 200 - tooltipLayout.implicitHeight)
129
130 WrapperMouseArea {
131 id: tooltipMouseArea
126 132
127 WrapperItem { 133 hoverEnabled: true
128 id: tooltipContent 134 enabled: true
129 135
130 margin: 8 136 WrapperItem {
131 bottomMargin: 8 + Math.max(0, 200 - tooltipLayout.implicitHeight) 137 margin: 8
138 bottomMargin: 8
132 139
133 GridLayout { 140 GridLayout {
134 id: tooltipLayout 141 id: tooltipLayout
135 142
136 columns: 4 143 columns: 4
137 144
138 Repeater { 145 Repeater {
139 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device") 146 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
140 147
141 Item { 148 Item {
142 id: descItem 149 id: descItem
143 150
144 required property var modelData 151 required property var modelData
145 required property int index 152 required property int index
146 153
147 Layout.column: 0 154 Layout.column: 0
148 Layout.row: index 155 Layout.row: index
149 156
150 implicitWidth: descText.contentWidth 157 implicitWidth: descText.contentWidth
151 implicitHeight: descText.contentHeight 158 implicitHeight: descText.contentHeight
152 159
153 Text { 160 Text {
154 id: descText 161 id: descText
155 162
156 color: "white" 163 color: "white"
157 font.pointSize: 10 164 font.pointSize: 10
158 font.family: "Fira Sans" 165 font.family: "Fira Sans"
159 166
160 text: descItem.modelData.description 167 text: descItem.modelData.description
168 }
161 } 169 }
162 } 170 }
163 }
164 171
165 Repeater { 172 Repeater {
166 id: defaultSinkRepeater 173 id: defaultSinkRepeater
167 174
168 model: { 175 model: {
169 Array.from(Pipewire.devices.values) 176 Array.from(Pipewire.devices.values)
170 .filter(dev => dev.type == "Audio/Device") 177 .filter(dev => dev.type == "Audio/Device")
171 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id )); 178 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id ));
172 } 179 }
173 180
174 Item { 181 Item {
175 id: defaultSinkItem 182 id: defaultSinkItem
176 183
177 required property var modelData 184 required property var modelData
178 required property int index 185 required property int index
179 186
180 PwObjectTracker { 187 visible: Boolean(modelData)
181 objects: [defaultSinkItem.modelData]
182 }
183 188
184 Layout.column: 1 189 PwObjectTracker {
185 Layout.row: index 190 objects: [defaultSinkItem.modelData]
191 }
186 192
187 Layout.fillHeight: true 193 Layout.column: 1
194 Layout.row: index
188 195
189 implicitWidth: 16 + 8 196 Layout.fillHeight: true
190 197
191 WrapperMouseArea { 198 implicitWidth: 16 + 8
192 id: defaultSinkMouseArea
193 199
194 anchors.fill: parent 200 WrapperMouseArea {
195 hoverEnabled: true 201 id: defaultSinkMouseArea
196 cursorShape: Qt.PointingHandCursor
197 202
198 onClicked: { 203 anchors.fill: parent
199 Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData 204 hoverEnabled: true
200 } 205 cursorShape: Qt.PointingHandCursor
201 206
202 Rectangle { 207 onClicked: {
203 id: defaultSinkWidget 208 Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData
209 }
204 210
205 anchors.fill: parent 211 onWheel: event => scrollVolume(event);
206 color: { 212 property real sensitivity: (1 / 40) / 120
207 if (defaultSinkMouseArea.containsMouse) 213 function scrollVolume(event) {
208 return "#33808080"; 214 defaultSinkItem.modelData.audio.volume += event.angleDelta.y * sensitivity;
209 return "transparent";
210 } 215 }
211 216
212 MaterialDesignIcon { 217 Rectangle {
213 width: 16 218 id: defaultSinkWidget
214 height: 16 219
220 anchors.fill: parent
221 color: {
222 if (defaultSinkMouseArea.containsMouse)
223 return "#33808080";
224 return "transparent";
225 }
226
227 MaterialDesignIcon {
228 width: 16
229 height: 16
230 anchors.centerIn: parent
231
232 icon: {
233 if (defaultSinkItem.modelData?.id == Pipewire.defaultAudioSink?.id)
234 return "speaker";
235 return "speaker-off";
236 }
237 color: icon == "speaker" ? "white" : "#555"
238 }
239 }
240 }
241
242 PopupWindow {
243 id: volumeTooltip
244
245 property bool nextVisible: defaultSinkMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse
246
247 anchor {
248 item: defaultSinkMouseArea
249 edges: Edges.Bottom | Edges.Left
250 }
251 visible: false
252
253 onNextVisibleChanged: volumeHangTimer.restart()
254
255 onVisibleChanged: tooltip.openPopup = volumeTooltip.visible
256
257 Timer {
258 id: volumeHangTimer
259 interval: 100
260 onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible
261 }
262
263 implicitWidth: volumeTooltipText.contentWidth + 16
264 implicitHeight: volumeTooltipText.contentHeight + 16
265 color: "black"
266
267 WrapperMouseArea {
268 id: volumeTooltipMouseArea
269
270 hoverEnabled: true
271 enabled: true
272
273 onWheel: event => defaultSinkMouseArea.scrollVolume(event);
274
215 anchors.centerIn: parent 275 anchors.centerIn: parent
216 276
217 icon: { 277 Text {
218 if (defaultSinkItem.modelData?.id == Pipewire.defaultAudioSink?.id) 278 id: volumeTooltipText
219 return "speaker"; 279
220 return "speaker-off"; 280 font.pointSize: 10
281 font.family: "Fira Sans"
282 color: "white"
283
284 text: `${Math.round(defaultSinkItem.modelData?.audio?.volume * 100)}%`
221 } 285 }
222 color: icon == "speaker" ? "white" : "#555"
223 } 286 }
224 } 287 }
225 } 288 }
226 } 289 }
227 }
228 290
229 Repeater { 291 Repeater {
230 id: defaultSourceRepeater 292 id: defaultSourceRepeater
231 293
232 model: { 294 model: {
233 Array.from(Pipewire.devices.values) 295 Array.from(Pipewire.devices.values)
234 .filter(dev => dev.type == "Audio/Device") 296 .filter(dev => dev.type == "Audio/Device")
235 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id )); 297 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id ));
236 } 298 }
237 299
238 Item { 300 Item {
239 id: defaultSourceItem 301 id: defaultSourceItem
240 302
241 required property var modelData 303 required property var modelData
242 required property int index 304 required property int index
243 305
244 PwObjectTracker { 306 visible: Boolean(modelData)
245 objects: [defaultSourceItem.modelData] 307
246 } 308 PwObjectTracker {
309 objects: [defaultSourceItem.modelData]
310 }
311
312 Layout.column: 2
313 Layout.row: index
247 314
248 Layout.column: 2 315 Layout.fillHeight: true
249 Layout.row: index 316
317 implicitWidth: 16 + 8
318
319 WrapperMouseArea {
320 id: defaultSourceMouseArea
321
322 anchors.fill: parent
323 hoverEnabled: true
324 cursorShape: Qt.PointingHandCursor
250 325
251 Layout.fillHeight: true 326 onClicked: {
327 Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData
328 }
252 329
253 implicitWidth: 16 + 8 330 onWheel: event => scrollVolume(event);
331 property real sensitivity: (1 / 40) / 120
332 function scrollVolume(event) {
333 defaultSourceItem.modelData.audio.volume += event.angleDelta.y * sensitivity;
334 }
254 335
255 WrapperMouseArea { 336 Rectangle {
256 id: defaultSourceMouseArea 337 id: defaultSourceWidget
257 338
258 anchors.fill: parent 339 anchors.fill: parent
259 hoverEnabled: true 340 color: {
260 cursorShape: Qt.PointingHandCursor 341 if (defaultSourceMouseArea.containsMouse)
342 return "#33808080";
343 return "transparent";
344 }
261 345
262 onClicked: { 346 MaterialDesignIcon {
263 Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData 347 width: 16
348 height: 16
349 anchors.centerIn: parent
350
351 icon: {
352 if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id)
353 return "microphone";
354 return "microphone-off";
355 }
356 color: icon == "microphone" ? "white" : "#555"
357 }
358 }
264 } 359 }
265 360
266 Rectangle { 361 PopupWindow {
267 id: defaultSourceWidget 362 id: volumeTooltip
268 363
269 anchors.fill: parent 364 property bool nextVisible: defaultSourceMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse
270 color: { 365
271 if (defaultSourceMouseArea.containsMouse) 366 anchor {
272 return "#33808080"; 367 item: defaultSourceMouseArea
273 return "transparent"; 368 edges: Edges.Bottom | Edges.Left
274 } 369 }
370 visible: false
371
372 onNextVisibleChanged: volumeHangTimer.restart()
373
374 onVisibleChanged: tooltip.openPopup = volumeTooltip.visible
375
376 Timer {
377 id: volumeHangTimer
378 interval: 100
379 onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible
380 }
381
382 implicitWidth: volumeTooltipText.contentWidth + 16
383 implicitHeight: volumeTooltipText.contentHeight + 16
384 color: "black"
385
386 WrapperMouseArea {
387 id: volumeTooltipMouseArea
388
389 hoverEnabled: true
390 enabled: true
391
392 onWheel: event => defaultSourceMouseArea.scrollVolume(event);
275 393
276 MaterialDesignIcon {
277 width: 16
278 height: 16
279 anchors.centerIn: parent 394 anchors.centerIn: parent
280 395
281 icon: { 396 Text {
282 if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id) 397 id: volumeTooltipText
283 return "microphone"; 398
284 return "microphone-off"; 399 font.pointSize: 10
400 font.family: "Fira Sans"
401 color: "white"
402
403 text: `${Math.round(defaultSourceItem.modelData?.audio?.volume * 100)}%`
285 } 404 }
286 color: icon == "microphone" ? "white" : "#555"
287 } 405 }
288 } 406 }
289 } 407 }
290 } 408 }
291 }
292 409
293 Repeater { 410 Repeater {
294 id: profileRepeater 411 id: profileRepeater
295 412
296 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device") 413 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
297 414
298 Item { 415 Item {
299 id: profileItem 416 id: profileItem
300 417
301 required property var modelData 418 required property var modelData
302 required property int index 419 required property int index
303 420
304 PwObjectTracker { 421 PwObjectTracker {
305 objects: [profileItem.modelData] 422 objects: [profileItem.modelData]
306 } 423 }
307 424
308 Layout.column: 3 425 Layout.column: 3
309 Layout.row: index 426 Layout.row: index
310 427
311 Layout.fillWidth: true 428 Layout.fillWidth: true
312 429
313 implicitWidth: Math.max(profileBox.implicitWidth, 300) 430 implicitWidth: Math.max(profileBox.implicitWidth, 300)
314 implicitHeight: profileBox.height 431 implicitHeight: profileBox.height
315 432
316 ComboBox { 433 ComboBox {
317 id: profileBox 434 id: profileBox
318 435
319 model: profileItem.modelData.profiles 436 model: profileItem.modelData.profiles
320 437
321 textRole: "description" 438 textRole: "description"
322 valueRole: "index" 439 valueRole: "index"
323 onActivated: profileItem.modelData.setProfile(currentValue) 440 onActivated: profileItem.modelData.setProfile(currentValue)
324 441
325 anchors.fill: parent 442 anchors.fill: parent
326 443
327 implicitContentWidthPolicy: ComboBox.WidestText 444 implicitContentWidthPolicy: ComboBox.WidestText
328 445
329 Connections { 446 Connections {
330 target: profileItem.modelData 447 target: profileItem.modelData
331 function onCurrentProfileChanged() { 448 function onCurrentProfileChanged() {
449 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
450 }
451 }
452 Component.onCompleted: {
332 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile); 453 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
333 } 454 }
334 }
335 Component.onCompleted: {
336 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
337 }
338 455
339 Connections { 456 Connections {
340 target: profileBox.popup 457 target: profileBox.popup
341 function onVisibleChanged() { 458 function onVisibleChanged() {
342 tooltip.openPopup = profileBox.popup.visible 459 tooltip.openPopup = profileBox.popup.visible
460 }
343 } 461 }
344 } 462 }
345 } 463 }
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml b/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml
index 87c7c05b..8318df50 100644
--- a/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml
@@ -23,14 +23,21 @@ Singleton {
23 return val; 23 return val;
24 } 24 }
25 25
26 Component.onCompleted: root.currBrightness = root.calcCurrBrightness()
27 Connections { 26 Connections {
28 target: currFile 27 target: currFile
29 function onLoaded() { root.currBrightness = root.calcCurrBrightness(); } 28 function onLoaded() {
29 const b = root.calcCurrBrightness();
30 if (typeof b !== 'undefined')
31 root.currBrightness = b;
32 }
30 } 33 }
31 Connections { 34 Connections {
32 target: maxFile 35 target: maxFile
33 function onLoaded() { root.currBrightness = root.calcCurrBrightness(); } 36 function onLoaded() {
37 const b = root.calcCurrBrightness();
38 if (typeof b !== 'undefined')
39 root.currBrightness = b;
40 }
34 } 41 }
35 42
36 onCurrBrightnessChanged: { 43 onCurrBrightnessChanged: {
diff --git a/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
index 16fb5dea..653f4763 100644
--- a/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
+++ b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
@@ -122,6 +122,8 @@ Scope {
122 } else if (root.show == "source") { 122 } else if (root.show == "source") {
123 if (!Pipewire.defaultAudioSource || Pipewire.defaultAudioSource.audio.muted) 123 if (!Pipewire.defaultAudioSource || Pipewire.defaultAudioSource.audio.muted)
124 return "microphone-off"; 124 return "microphone-off";
125 if (Pipewire.defaultAudioSource.audio.volume > 1)
126 return "microphone-plus";
125 return "microphone"; 127 return "microphone";
126 } 128 }
127 return "volume-high"; 129 return "volume-high";
@@ -148,7 +150,7 @@ Scope {
148 if (root.show == "sink") 150 if (root.show == "sink")
149 return parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0); 151 return parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0);
150 else if (root.show == "source") 152 else if (root.show == "source")
151 return parent.width * (Pipewire.defaultAudioSource?.audio.volume ?? 0); 153 return parent.width * Math.min(1, (Pipewire.defaultAudioSource?.audio.volume ?? 0));
152 return 0; 154 return 0;
153 } 155 }
154 } 156 }