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.qml354
1 files changed, 354 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..5bf0ac3a
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
@@ -0,0 +1,354 @@
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 / 20) / 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 width: 16
54 height: 16
55 anchors.centerIn: parent
56
57 icon: {
58 if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted)
59 return "volume-off";
60 if (Pipewire.defaultAudioSink.audio.volume <= 0.33)
61 return "volume-low";
62 if (Pipewire.defaultAudioSink.audio.volume <= 0.67)
63 return "volume-medium";
64 return "volume-high";
65 }
66 color: "#555"
67 }
68 }
69 }
70 }
71
72 Loader {
73 id: tooltipLoader
74
75 active: false
76
77 Connections {
78 target: widgetMouseArea
79 function onContainsMouseChanged() {
80 if (widgetMouseArea.containsMouse)
81 tooltipLoader.active = true;
82 }
83 }
84
85 PwObjectTracker {
86 objects: Pipewire.devices
87 }
88 PwObjectTracker {
89 objects: Pipewire.nodes
90 }
91
92 sourceComponent: PopupWindow {
93 id: tooltip
94
95 property bool openPopup: false
96 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse || openPopup
97
98 anchor {
99 item: widgetMouseArea
100 edges: Edges.Bottom | Edges.Left
101 }
102 visible: false
103
104 onNextVisibleChanged: hangTimer.restart()
105
106 Timer {
107 id: hangTimer
108 interval: 100
109 onTriggered: {
110 tooltip.visible = tooltip.nextVisible;
111 if (!tooltip.visible)
112 tooltipLoader.active = false;
113 }
114 }
115
116 implicitWidth: tooltipContent.width
117 implicitHeight: tooltipContent.height
118 color: "black"
119
120 WrapperMouseArea {
121 id: tooltipMouseArea
122
123 hoverEnabled: true
124 enabled: true
125
126 anchors.fill: parent
127
128 WrapperItem {
129 id: tooltipContent
130
131 margin: 8
132 bottomMargin: 8 + Math.max(0, 200 - tooltipLayout.implicitHeight)
133
134 GridLayout {
135 id: tooltipLayout
136
137 columns: 4
138
139 Repeater {
140 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
141
142 Item {
143 id: descItem
144
145 required property var modelData
146 required property int index
147
148 Layout.column: 0
149 Layout.row: index
150
151 implicitWidth: descText.contentWidth
152 implicitHeight: descText.contentHeight
153
154 Text {
155 id: descText
156
157 color: "white"
158 font.pointSize: 10
159 font.family: "Fira Sans"
160
161 text: descItem.modelData.description
162 }
163 }
164 }
165
166 Repeater {
167 id: defaultSinkRepeater
168
169 model: {
170 Array.from(Pipewire.devices.values)
171 .filter(dev => dev.type == "Audio/Device")
172 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id ));
173 }
174
175 Item {
176 id: defaultSinkItem
177
178 required property var modelData
179 required property int index
180
181 PwObjectTracker {
182 objects: [defaultSinkItem.modelData]
183 }
184
185 Layout.column: 1
186 Layout.row: index
187
188 Layout.fillHeight: true
189
190 implicitWidth: 16 + 8
191
192 WrapperMouseArea {
193 id: defaultSinkMouseArea
194
195 anchors.fill: parent
196 hoverEnabled: true
197 cursorShape: Qt.PointingHandCursor
198
199 onClicked: {
200 Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData
201 }
202
203 Rectangle {
204 id: defaultSinkWidget
205
206 anchors.fill: parent
207 color: {
208 if (defaultSinkMouseArea.containsMouse)
209 return "#33808080";
210 return "transparent";
211 }
212
213 MaterialDesignIcon {
214 width: 16
215 height: 16
216 anchors.centerIn: parent
217
218 icon: {
219 if (defaultSinkItem.modelData?.id == Pipewire.defaultAudioSink?.id)
220 return "speaker";
221 return "speaker-off";
222 }
223 color: icon == "speaker" ? "white" : "#555"
224 }
225 }
226 }
227 }
228 }
229
230 Repeater {
231 id: defaultSourceRepeater
232
233 model: {
234 Array.from(Pipewire.devices.values)
235 .filter(dev => dev.type == "Audio/Device")
236 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id ));
237 }
238
239 Item {
240 id: defaultSourceItem
241
242 required property var modelData
243 required property int index
244
245 PwObjectTracker {
246 objects: [defaultSourceItem.modelData]
247 }
248
249 Layout.column: 2
250 Layout.row: index
251
252 Layout.fillHeight: true
253
254 implicitWidth: 16 + 8
255
256 WrapperMouseArea {
257 id: defaultSourceMouseArea
258
259 anchors.fill: parent
260 hoverEnabled: true
261 cursorShape: Qt.PointingHandCursor
262
263 onClicked: {
264 Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData
265 }
266
267 Rectangle {
268 id: defaultSourceWidget
269
270 anchors.fill: parent
271 color: {
272 if (defaultSourceMouseArea.containsMouse)
273 return "#33808080";
274 return "transparent";
275 }
276
277 MaterialDesignIcon {
278 width: 16
279 height: 16
280 anchors.centerIn: parent
281
282 icon: {
283 if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id)
284 return "microphone";
285 return "microphone-off";
286 }
287 color: icon == "microphone" ? "white" : "#555"
288 }
289 }
290 }
291 }
292 }
293
294 Repeater {
295 id: profileRepeater
296
297 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
298
299 Item {
300 id: profileItem
301
302 required property var modelData
303 required property int index
304
305 PwObjectTracker {
306 objects: [profileItem.modelData]
307 }
308
309 Layout.column: 3
310 Layout.row: index
311
312 Layout.fillWidth: true
313
314 implicitWidth: Math.max(profileBox.implicitWidth, 300)
315 implicitHeight: profileBox.height
316
317 ComboBox {
318 id: profileBox
319
320 model: profileItem.modelData.profiles
321
322 textRole: "description"
323 valueRole: "index"
324 onActivated: profileItem.modelData.setProfile(currentValue)
325
326 anchors.fill: parent
327
328 implicitContentWidthPolicy: ComboBox.WidestText
329
330 Connections {
331 target: profileItem.modelData
332 function onCurrentProfileChanged() {
333 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
334 }
335 }
336 Component.onCompleted: {
337 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
338 }
339
340 Connections {
341 target: profileBox.popup
342 function onVisibleChanged() {
343 tooltip.openPopup = profileBox.popup.visible
344 }
345 }
346 }
347 }
348 }
349 }
350 }
351 }
352 }
353 }
354}