summaryrefslogtreecommitdiff
path: root/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
diff options
context:
space:
mode:
Diffstat (limited to 'accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml')
-rw-r--r--accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml483
1 files changed, 483 insertions, 0 deletions
diff --git a/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
new file mode 100644
index 00000000..9c6b65a4
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
@@ -0,0 +1,483 @@
1import QtQuick
2import QtQuick.Layouts
3import QtQuick.Controls.Fusion
4import Quickshell
5import Quickshell.Services.Pipewire
6import Quickshell.Widgets
7
8Item {
9 height: parent.height
10 width: volumeIcon.width + 8
11 anchors.verticalCenter: parent.verticalCenter
12
13 PwObjectTracker {
14 objects: [Pipewire.defaultAudioSink]
15 }
16
17 WrapperMouseArea {
18 id: widgetMouseArea
19
20 anchors.fill: parent
21 hoverEnabled: true
22 cursorShape: Qt.PointingHandCursor
23
24 onClicked: {
25 if (!Pipewire.defaultAudioSink)
26 return;
27 Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted;
28 }
29
30 property real sensitivity: (1 / 40) / 120
31 onWheel: event => {
32 if (!Pipewire.defaultAudioSink)
33 return;
34 Pipewire.defaultAudioSink.audio.volume += event.angleDelta.y * sensitivity;
35 }
36
37 Rectangle {
38 id: volumeWidget
39
40 anchors.fill: parent
41 color: {
42 if (widgetMouseArea.containsMouse)
43 return "#33808080";
44 return "transparent";
45 }
46
47 Item {
48 anchors.fill: parent
49
50 MaterialDesignIcon {
51 id: volumeIcon
52
53 implicitSize: 14
54 anchors.centerIn: parent
55
56 icon: {
57 if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted)
58 return "volume-off";
59 if (Pipewire.defaultAudioSink.audio.volume <= 0.33)
60 return "volume-low";
61 if (Pipewire.defaultAudioSink.audio.volume <= 0.67)
62 return "volume-medium";
63 return "volume-high";
64 }
65 color: "#555"
66 }
67 }
68 }
69 }
70
71 Loader {
72 id: tooltipLoader
73
74 active: false
75
76 Connections {
77 target: widgetMouseArea
78 function onContainsMouseChanged() {
79 if (widgetMouseArea.containsMouse)
80 tooltipLoader.active = true;
81 }
82 }
83
84 PwObjectTracker {
85 objects: Pipewire.devices
86 }
87 PwObjectTracker {
88 objects: Pipewire.nodes
89 }
90
91 sourceComponent: PopupWindow {
92 id: tooltip
93
94 property bool openPopup: false
95 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse || openPopup
96
97 anchor {
98 item: widgetMouseArea
99 edges: Edges.Bottom | Edges.Left
100 }
101 visible: false
102
103 onNextVisibleChanged: hangTimer.restart()
104
105 Timer {
106 id: hangTimer
107 interval: 100
108 onTriggered: {
109 tooltip.visible = tooltip.nextVisible;
110 if (!tooltip.visible)
111 tooltipLoader.active = false;
112 }
113 }
114
115 implicitWidth: tooltipContent.width
116 implicitHeight: tooltipContent.height
117 color: "transparent"
118
119 Rectangle {
120 width: tooltip.width
121 height: tooltipLayout.childrenRect.height + 16
122 color: "black"
123 }
124
125 WrapperItem {
126 id: tooltipContent
127
128 bottomMargin: Math.max(0, 200 - tooltipLayout.implicitHeight)
129
130 WrapperMouseArea {
131 id: tooltipMouseArea
132
133 hoverEnabled: true
134 enabled: true
135
136 WrapperItem {
137 margin: 8
138 bottomMargin: 8
139
140 GridLayout {
141 id: tooltipLayout
142
143 columns: 4
144
145 Repeater {
146 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
147
148 Item {
149 id: descItem
150
151 required property var modelData
152 required property int index
153
154 Layout.column: 0
155 Layout.row: index
156
157 implicitWidth: descText.contentWidth
158 implicitHeight: descText.contentHeight
159
160 Text {
161 id: descText
162
163 color: "white"
164 font.pointSize: 10
165 font.family: "Fira Sans"
166
167 text: descItem.modelData.description
168 }
169 }
170 }
171
172 Repeater {
173 id: defaultSinkRepeater
174
175 model: {
176 Array.from(Pipewire.devices.values)
177 .filter(dev => dev.type == "Audio/Device")
178 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id ));
179 }
180
181 Item {
182 id: defaultSinkItem
183
184 required property var modelData
185 required property int index
186
187 visible: Boolean(modelData)
188
189 PwObjectTracker {
190 objects: [defaultSinkItem.modelData]
191 }
192
193 Layout.column: 1
194 Layout.row: index
195
196 Layout.fillHeight: true
197
198 implicitWidth: 16 + 8
199
200 WrapperMouseArea {
201 id: defaultSinkMouseArea
202
203 anchors.fill: parent
204 hoverEnabled: true
205 cursorShape: Qt.PointingHandCursor
206
207 onClicked: {
208 Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData
209 }
210
211 onWheel: event => scrollVolume(event);
212 property real sensitivity: (1 / 40) / 120
213 function scrollVolume(event) {
214 defaultSinkItem.modelData.audio.volume += event.angleDelta.y * sensitivity;
215 }
216
217 Rectangle {
218 id: defaultSinkWidget
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
275 anchors.fill: parent
276
277 Item {
278 anchors.fill: parent
279
280 Text {
281 id: volumeTooltipText
282
283 anchors.centerIn: parent
284
285 font.pointSize: 10
286 font.family: "Fira Sans"
287 color: "white"
288
289 text: `${Math.round(defaultSinkItem.modelData?.audio?.volume * 100)}%`
290 }
291 }
292 }
293 }
294 }
295 }
296
297 Repeater {
298 id: defaultSourceRepeater
299
300 model: {
301 Array.from(Pipewire.devices.values)
302 .filter(dev => dev.type == "Audio/Device")
303 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id ));
304 }
305
306 Item {
307 id: defaultSourceItem
308
309 required property var modelData
310 required property int index
311
312 visible: Boolean(modelData)
313
314 PwObjectTracker {
315 objects: [defaultSourceItem.modelData]
316 }
317
318 Layout.column: 2
319 Layout.row: index
320
321 Layout.fillHeight: true
322
323 implicitWidth: 16 + 8
324
325 WrapperMouseArea {
326 id: defaultSourceMouseArea
327
328 anchors.fill: parent
329 hoverEnabled: true
330 cursorShape: Qt.PointingHandCursor
331
332 onClicked: {
333 Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData
334 }
335
336 onWheel: event => scrollVolume(event);
337 property real sensitivity: (1 / 40) / 120
338 function scrollVolume(event) {
339 defaultSourceItem.modelData.audio.volume += event.angleDelta.y * sensitivity;
340 }
341
342 Rectangle {
343 id: defaultSourceWidget
344
345 anchors.fill: parent
346 color: {
347 if (defaultSourceMouseArea.containsMouse)
348 return "#33808080";
349 return "transparent";
350 }
351
352 MaterialDesignIcon {
353 width: 16
354 height: 16
355 anchors.centerIn: parent
356
357 icon: {
358 if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id)
359 return "microphone";
360 return "microphone-off";
361 }
362 color: icon == "microphone" ? "white" : "#555"
363 }
364 }
365 }
366
367 PopupWindow {
368 id: volumeTooltip
369
370 property bool nextVisible: defaultSourceMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse
371
372 anchor {
373 item: defaultSourceMouseArea
374 edges: Edges.Bottom | Edges.Left
375 }
376 visible: false
377
378 onNextVisibleChanged: volumeHangTimer.restart()
379
380 onVisibleChanged: tooltip.openPopup = volumeTooltip.visible
381
382 Timer {
383 id: volumeHangTimer
384 interval: 100
385 onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible
386 }
387
388 implicitWidth: volumeTooltipText.contentWidth + 16
389 implicitHeight: volumeTooltipText.contentHeight + 16
390 color: "black"
391
392 WrapperMouseArea {
393 id: volumeTooltipMouseArea
394
395 hoverEnabled: true
396 enabled: true
397
398 onWheel: event => defaultSourceMouseArea.scrollVolume(event);
399
400 anchors.fill: parent
401
402 Item {
403 anchors.fill: parent
404
405 Text {
406 id: volumeTooltipText
407
408 anchors.centerIn: parent
409
410 font.pointSize: 10
411 font.family: "Fira Sans"
412 color: "white"
413
414 text: `${Math.round(defaultSourceItem.modelData?.audio?.volume * 100)}%`
415 }
416 }
417 }
418 }
419 }
420 }
421
422 Repeater {
423 id: profileRepeater
424
425 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
426
427 Item {
428 id: profileItem
429
430 required property var modelData
431 required property int index
432
433 PwObjectTracker {
434 objects: [profileItem.modelData]
435 }
436
437 Layout.column: 3
438 Layout.row: index
439
440 Layout.fillWidth: true
441
442 implicitWidth: Math.max(profileBox.implicitWidth, 300)
443 implicitHeight: profileBox.height
444
445 ComboBox {
446 id: profileBox
447
448 model: profileItem.modelData.profiles
449
450 textRole: "description"
451 valueRole: "index"
452 onActivated: profileItem.modelData.setProfile(currentValue)
453
454 anchors.fill: parent
455
456 implicitContentWidthPolicy: ComboBox.WidestText
457
458 Connections {
459 target: profileItem.modelData
460 function onCurrentProfileChanged() {
461 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
462 }
463 }
464 Component.onCompleted: {
465 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
466 }
467
468 Connections {
469 target: profileBox.popup
470 function onVisibleChanged() {
471 tooltip.openPopup = profileBox.popup.visible
472 }
473 }
474 }
475 }
476 }
477 }
478 }
479 }
480 }
481 }
482 }
483}