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
|
||||
}
|
||||
|
||||
Battery {
|
||||
id: widgetsBattery
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Brightness {
|
||||
id: widgetsBrightness
|
||||
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
|
||||
text: ""
|
||||
tooltipVisible: false
|
||||
targetItem: undefined
|
||||
targetItem: null
|
||||
delay: 100
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import QtQuick
|
|||
import Quickshell
|
||||
import qs.Settings
|
||||
import qs.Components
|
||||
import qs.Bar.Modules
|
||||
|
||||
Item {
|
||||
id: volumeDisplay
|
||||
|
|
@ -24,10 +25,23 @@ Item {
|
|||
StyledTooltip {
|
||||
id: volumeTooltip
|
||||
text: "Volume: " + volume + "%\nScroll up/down to change volume"
|
||||
tooltipVisible: false
|
||||
tooltipVisible: !ioSelector.visible && volumeDisplay.containsMouse
|
||||
targetItem: pillIndicator
|
||||
delay: 200
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (ioSelector.visible) {
|
||||
ioSelector.dismiss();
|
||||
} else {
|
||||
ioSelector.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
|
@ -54,8 +68,8 @@ Item {
|
|||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton // Accept wheel events only
|
||||
propagateComposedEvents: true
|
||||
onEntered: volumeTooltip.tooltipVisible = true
|
||||
onExited: volumeTooltip.tooltipVisible = false
|
||||
onEntered: volumeDisplay.containsMouse = true
|
||||
onExited: volumeDisplay.containsMouse = false
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onWheel:(wheel) => {
|
||||
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