Add Audio Input/Output selector, add (probably) working battery indicator
This commit is contained in:
parent
ba93df4a1f
commit
f75ff03281
5 changed files with 394 additions and 4 deletions
|
|
@ -87,6 +87,11 @@ Scope {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Battery {
|
||||||
|
id: widgetsBattery
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
Brightness {
|
Brightness {
|
||||||
id: widgetsBrightness
|
id: widgetsBrightness
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
|
||||||
283
Bar/Modules/AudioDeviceSelector.qml
Normal file
283
Bar/Modules/AudioDeviceSelector.qml
Normal file
|
|
@ -0,0 +1,283 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Components
|
||||||
|
import qs.Settings
|
||||||
|
|
||||||
|
PanelWithOverlay {
|
||||||
|
id: ioSelector
|
||||||
|
signal closed()
|
||||||
|
property int tabIndex: 0
|
||||||
|
property Item anchorItem: null
|
||||||
|
|
||||||
|
// Bind all Pipewire nodes so their properties are valid
|
||||||
|
PwObjectTracker {
|
||||||
|
id: nodeTracker
|
||||||
|
objects: Pipewire.nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
color: Theme.backgroundPrimary
|
||||||
|
radius: 20
|
||||||
|
width: 340
|
||||||
|
height: 340
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: 4
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 16
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
// Tabs centered inside the window
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Tabs {
|
||||||
|
id: ioTabs
|
||||||
|
tabsModel: [
|
||||||
|
{ label: "Output", icon: "volume_up" },
|
||||||
|
{ label: "Input", icon: "mic" }
|
||||||
|
]
|
||||||
|
currentIndex: tabIndex
|
||||||
|
onTabChanged: {
|
||||||
|
tabIndex = currentIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add vertical space between tabs and entries
|
||||||
|
Item { height: 36; Layout.fillWidth: true }
|
||||||
|
|
||||||
|
// Output Devices
|
||||||
|
Flickable {
|
||||||
|
id: sinkList
|
||||||
|
visible: tabIndex === 0
|
||||||
|
contentHeight: sinkColumn.height
|
||||||
|
clip: true
|
||||||
|
interactive: contentHeight > height
|
||||||
|
width: parent.width
|
||||||
|
height: 220
|
||||||
|
ScrollBar.vertical: ScrollBar {}
|
||||||
|
ColumnLayout {
|
||||||
|
id: sinkColumn
|
||||||
|
width: sinkList.width
|
||||||
|
spacing: 6
|
||||||
|
Repeater {
|
||||||
|
model: ioSelector.sinkNodes()
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 36
|
||||||
|
color: "transparent"
|
||||||
|
radius: 6
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6
|
||||||
|
spacing: 8
|
||||||
|
Text {
|
||||||
|
text: "volume_up"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 1
|
||||||
|
Text {
|
||||||
|
text: modelData.nickname || modelData.description || modelData.name
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: modelData.description !== modelData.nickname ? modelData.description : ""
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: Theme.textSecondary
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
Rectangle {
|
||||||
|
visible: Pipewire.preferredDefaultAudioSink !== modelData
|
||||||
|
width: 60; height: 20
|
||||||
|
radius: 4
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
border.color: Theme.accentPrimary
|
||||||
|
border.width: 1
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Set"
|
||||||
|
color: Theme.onAccent
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: Pipewire.preferredDefaultAudioSink = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: "(Current)"
|
||||||
|
visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
font.pixelSize: 10
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input Devices
|
||||||
|
Flickable {
|
||||||
|
id: sourceList
|
||||||
|
visible: tabIndex === 1
|
||||||
|
contentHeight: sourceColumn.height
|
||||||
|
clip: true
|
||||||
|
interactive: contentHeight > height
|
||||||
|
width: parent.width
|
||||||
|
height: 220
|
||||||
|
ScrollBar.vertical: ScrollBar {}
|
||||||
|
ColumnLayout {
|
||||||
|
id: sourceColumn
|
||||||
|
width: sourceList.width
|
||||||
|
spacing: 6
|
||||||
|
Repeater {
|
||||||
|
model: ioSelector.sourceNodes()
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 36
|
||||||
|
color: "transparent"
|
||||||
|
radius: 6
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6
|
||||||
|
spacing: 8
|
||||||
|
Text {
|
||||||
|
text: "mic"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 1
|
||||||
|
Text {
|
||||||
|
text: modelData.nickname || modelData.description || modelData.name
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: modelData.description !== modelData.nickname ? modelData.description : ""
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: Theme.textSecondary
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
Rectangle {
|
||||||
|
visible: Pipewire.preferredDefaultAudioSource !== modelData
|
||||||
|
width: 60; height: 20
|
||||||
|
radius: 4
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
border.color: Theme.accentPrimary
|
||||||
|
border.width: 1
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Set"
|
||||||
|
color: Theme.onAccent
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: Pipewire.preferredDefaultAudioSource = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: "(Current)"
|
||||||
|
visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id
|
||||||
|
color: Theme.accentPrimary
|
||||||
|
font.pixelSize: 10
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sinkNodes() {
|
||||||
|
let nodes = Pipewire.nodes && Pipewire.nodes.values
|
||||||
|
? Pipewire.nodes.values.filter(function(n) { return n.isSink && n.audio })
|
||||||
|
: [];
|
||||||
|
if (Pipewire.defaultAudioSink) {
|
||||||
|
nodes = nodes.slice().sort(function(a, b) {
|
||||||
|
if (a.id === Pipewire.defaultAudioSink.id) return -1;
|
||||||
|
if (b.id === Pipewire.defaultAudioSink.id) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
function sourceNodes() {
|
||||||
|
let nodes = Pipewire.nodes && Pipewire.nodes.values
|
||||||
|
? Pipewire.nodes.values.filter(function(n) { return !n.isSink && n.audio })
|
||||||
|
: [];
|
||||||
|
if (Pipewire.defaultAudioSource) {
|
||||||
|
nodes = nodes.slice().sort(function(a, b) {
|
||||||
|
if (a.id === Pipewire.defaultAudioSource.id) return -1;
|
||||||
|
if (b.id === Pipewire.defaultAudioSource.id) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (Pipewire.nodes && Pipewire.nodes.values) {
|
||||||
|
for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
|
||||||
|
var n = Pipewire.nodes.values[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Pipewire
|
||||||
|
function onReadyChanged() {
|
||||||
|
if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) {
|
||||||
|
for (var i = 0; i < Pipewire.nodes.values.length; ++i) {
|
||||||
|
var n = Pipewire.nodes.values[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onDefaultAudioSinkChanged() {
|
||||||
|
}
|
||||||
|
function onDefaultAudioSourceChanged() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
}
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Bar/Modules/Battery.qml
Normal file
81
Bar/Modules/Battery.qml
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Services.UPower
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Settings
|
||||||
|
import qs.Components
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: batteryWidget
|
||||||
|
property var battery: UPower.displayDevice
|
||||||
|
property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent
|
||||||
|
property real percent: isReady ? battery.percentage : 0
|
||||||
|
property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false
|
||||||
|
property bool show: isReady && percent > 0
|
||||||
|
|
||||||
|
// Choose icon based on charge and charging state
|
||||||
|
function batteryIcon() {
|
||||||
|
if (!show) return "";
|
||||||
|
if (percent >= 95) return "battery_full";
|
||||||
|
if (percent >= 80) return "battery_80";
|
||||||
|
if (percent >= 60) return "battery_60";
|
||||||
|
if (percent >= 50) return "battery_50";
|
||||||
|
if (percent >= 30) return "battery_30";
|
||||||
|
if (percent >= 20) return "battery_20";
|
||||||
|
return "battery_alert";
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: show
|
||||||
|
width: 22
|
||||||
|
height: 36
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 4
|
||||||
|
visible: show
|
||||||
|
Item {
|
||||||
|
height: 22
|
||||||
|
width: 22
|
||||||
|
Text {
|
||||||
|
text: batteryIcon()
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: charging ? Theme.accentPrimary : Theme.textPrimary
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
id: batteryMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onEntered: batteryWidget.containsMouse = true
|
||||||
|
onExited: batteryWidget.containsMouse = false
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool containsMouse: false
|
||||||
|
|
||||||
|
StyledTooltip {
|
||||||
|
id: batteryTooltip
|
||||||
|
text: {
|
||||||
|
let lines = [];
|
||||||
|
if (isReady) {
|
||||||
|
lines.push(charging ? "Charging" : "Discharging");
|
||||||
|
lines.push(Math.round(percent) + "%");
|
||||||
|
if (battery.changeRate !== undefined)
|
||||||
|
lines.push("Rate: " + battery.changeRate.toFixed(2) + " W");
|
||||||
|
if (battery.timeToEmpty > 0)
|
||||||
|
lines.push("Time left: " + Math.floor(battery.timeToEmpty / 60) + " min");
|
||||||
|
if (battery.timeToFull > 0)
|
||||||
|
lines.push("Time to full: " + Math.floor(battery.timeToFull / 60) + " min");
|
||||||
|
if (battery.healthPercentage !== undefined)
|
||||||
|
lines.push("Health: " + Math.round(battery.healthPercentage) + "%");
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
tooltipVisible: batteryWidget.containsMouse
|
||||||
|
targetItem: batteryWidget
|
||||||
|
delay: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -177,7 +177,7 @@ PanelWithOverlay {
|
||||||
id: holidayTooltip
|
id: holidayTooltip
|
||||||
text: ""
|
text: ""
|
||||||
tooltipVisible: false
|
tooltipVisible: false
|
||||||
targetItem: undefined
|
targetItem: null
|
||||||
delay: 100
|
delay: 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Settings
|
import qs.Settings
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
import qs.Bar.Modules
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: volumeDisplay
|
id: volumeDisplay
|
||||||
|
|
@ -24,10 +25,23 @@ Item {
|
||||||
StyledTooltip {
|
StyledTooltip {
|
||||||
id: volumeTooltip
|
id: volumeTooltip
|
||||||
text: "Volume: " + volume + "%\nScroll up/down to change volume"
|
text: "Volume: " + volume + "%\nScroll up/down to change volume"
|
||||||
tooltipVisible: false
|
tooltipVisible: !ioSelector.visible && volumeDisplay.containsMouse
|
||||||
targetItem: pillIndicator
|
targetItem: pillIndicator
|
||||||
delay: 200
|
delay: 200
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (ioSelector.visible) {
|
||||||
|
ioSelector.dismiss();
|
||||||
|
} else {
|
||||||
|
ioSelector.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|
@ -54,8 +68,8 @@ Item {
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
acceptedButtons: Qt.NoButton // Accept wheel events only
|
acceptedButtons: Qt.NoButton // Accept wheel events only
|
||||||
propagateComposedEvents: true
|
propagateComposedEvents: true
|
||||||
onEntered: volumeTooltip.tooltipVisible = true
|
onEntered: volumeDisplay.containsMouse = true
|
||||||
onExited: volumeTooltip.tooltipVisible = false
|
onExited: volumeDisplay.containsMouse = false
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onWheel:(wheel) => {
|
onWheel:(wheel) => {
|
||||||
if (!shell) return;
|
if (!shell) return;
|
||||||
|
|
@ -67,4 +81,11 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AudioDeviceSelector {
|
||||||
|
id: ioSelector
|
||||||
|
onClosed: ioSelector.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool containsMouse: false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue