diff options
| author | Leander Scherer <leander@schererleander.de> | 2026-05-30 15:35:27 +0200 |
|---|---|---|
| committer | Leander Scherer <leander@schererleander.de> | 2026-05-30 15:35:27 +0200 |
| commit | d2747e2ca1e211a32e91e44010f40a00e0ac97e4 (patch) | |
| tree | fb229d6a18541c7a5f1944390b21edde028955f9 /modules/system/quickshell/MicInput.qml | |
| parent | 51b3cbd50b92d026549ce3ebff17ca9b3344f441 (diff) | |
feat(quickshell): add popup controls and privacy indicators
Diffstat (limited to 'modules/system/quickshell/MicInput.qml')
| -rw-r--r-- | modules/system/quickshell/MicInput.qml | 204 |
1 files changed, 204 insertions, 0 deletions
diff --git a/modules/system/quickshell/MicInput.qml b/modules/system/quickshell/MicInput.qml new file mode 100644 index 0000000..2a1f653 --- /dev/null +++ b/modules/system/quickshell/MicInput.qml @@ -0,0 +1,204 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire + +Item { + id: root + width: indicator.visible ? indicator.width : 0 + height: parent.height + visible: root.micInUse + + readonly property PwNode source: Pipewire.defaultAudioSource + readonly property bool muted: source && source.audio ? source.audio.muted : true + readonly property real volume: source && source.audio ? source.audio.volume : 0 + readonly property bool micInUse: { + const nodes = Pipewire.nodes?.values || []; + for (const node of nodes) { + if (node && (node.type & PwNodeType.AudioInStream)) + return true; + } + return false; + } + + onMicInUseChanged: { + if (!micInUse && GlobalState.activePopup === "MicInput") + GlobalState.close(); + } + + PwObjectTracker { + objects: root.source ? [root.source] : [] + } + + function setVolume(value) { + if (source?.ready && source?.audio) { + source.audio.muted = false; + source.audio.volume = value; + } + } + + function toggleMute() { + if (source?.ready && source?.audio) { + source.audio.muted = !source.audio.muted; + } + } + + function getDeviceIcon(node) { + return node?.properties?.["device.icon-name"] ?? "audio-input-microphone"; + } + + Squircle { + id: indicator + anchors.verticalCenter: parent.verticalCenter + width: 28 + height: 22 + cornerRadius: 8 + fillColor: "#FFFF9500" + + Image { + anchors.centerIn: parent + width: 16 + height: 16 + source: Quickshell.iconPath(root.muted ? "microphone-sensitivity-muted-symbolic" : "audio-input-microphone-symbolic") + sourceSize: Qt.size(width, height) + smooth: true + mipmap: true + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + onClicked: mouse => { + if (mouse.button === Qt.RightButton) + root.toggleMute(); + else + GlobalState.toggle("MicInput"); + } + onWheel: wheel => { + const step = 0.05; + const next = wheel.angleDelta.y > 0 ? root.volume + step : root.volume - step; + root.setVolume(Math.max(0.0, Math.min(1.0, next))); + } + } + + AnchoredPopup { + popupName: "MicInput" + anchorWindow: barWindow + anchorItem: root + + PopupCard { + width: 280 + margins: 14 + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Text { + text: "Microphone" + color: Theme.text + font { + family: Theme.mainFont + pixelSize: 14 + weight: Font.DemiBold + } + } + + Item { + Layout.fillWidth: true + } + + Toggle { + checked: !root.muted + enabled: root.source !== null + onToggled: root.toggleMute() + } + } + + PillSlider { + id: micSlider + Layout.fillWidth: true + icon: "audio-input-microphone-symbolic" + value: root.volume + enabled: root.source !== null + colorProgress: "#FFFF9500" + onMoved: root.setVolume(value) + + Binding { + target: micSlider + property: "value" + value: root.volume + when: !micSlider.pressed + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Theme.border + } + + Text { + text: "Input Devices" + color: Theme.textMuted + font { + family: Theme.mainFont + pixelSize: 12 + weight: Font.DemiBold + } + Layout.leftMargin: 4 + } + + Repeater { + model: Pipewire.nodes + + delegate: Squircle { + id: inputItem + required property var modelData + readonly property bool isInput: modelData && (modelData.type & PwNodeType.AudioSource) && !modelData.isStream && modelData.name !== "Dummy-Driver" + + visible: isInput + Layout.fillWidth: true + Layout.preferredHeight: visible ? 40 : 0 + cornerRadius: 6 + fillColor: inputArea.containsMouse ? Theme.surfaceLighter : Theme.transparent + + RowLayout { + anchors { + fill: parent + margins: 8 + } + spacing: 12 + + IconCircle { + size: 24 + source: root.getDeviceIcon(inputItem.modelData) + active: root.source && root.source.id === inputItem.modelData.id + } + + Text { + Layout.fillWidth: true + text: inputItem.modelData.description || inputItem.modelData.nickname || inputItem.modelData.name + color: (root.source && root.source.id === inputItem.modelData.id) ? Theme.text : Theme.textDim + elide: Text.ElideRight + font { + family: Theme.mainFont + pixelSize: 13 + } + } + } + + MouseArea { + id: inputArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Pipewire.preferredDefaultAudioSource = inputItem.modelData + } + } + } + } + } +} |
