right sidebar: move audio controls to dialogs

This commit is contained in:
end-4
2025-10-19 23:58:45 +02:00
parent 0fe7bdc5b5
commit fec23cab8d
14 changed files with 248 additions and 388 deletions
@@ -139,6 +139,7 @@ Singleton {
property string networkEthernet: "kcmshell6 kcm_networkmanagement"
property string taskManager: "plasma-systemmonitor --page-name Processes"
property string terminal: "kitty -1" // This is only for shell actions
property string volumeMixer: `~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol"`
}
property JsonObject background: JsonObject {
@@ -165,7 +165,7 @@ Slider {
TrackDot {
required property real modelData
value: modelData
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenter: parent?.verticalCenter
}
}
}
@@ -0,0 +1,13 @@
import QtQuick
import Quickshell
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
StyledText {
text: "Section"
font {
pixelSize: Appearance.font.pixelSize.large
family: Appearance.font.family.title
}
}
@@ -13,70 +13,8 @@ Rectangle {
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
property int selectedTab: 0
property var tabButtonList: [
{"icon": "notifications", "name": Translation.tr("Notifications")},
{"icon": "volume_up", "name": Translation.tr("Audio")}
]
Keys.onPressed: (event) => {
if (event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) {
if (event.key === Qt.Key_PageDown) {
root.selectedTab = Math.min(root.selectedTab + 1, root.tabButtonList.length - 1)
} else if (event.key === Qt.Key_PageUp) {
root.selectedTab = Math.max(root.selectedTab - 1, 0)
}
event.accepted = true;
}
if (event.modifiers === Qt.ControlModifier) {
if (event.key === Qt.Key_Tab) {
root.selectedTab = (root.selectedTab + 1) % root.tabButtonList.length
} else if (event.key === Qt.Key_Backtab) {
root.selectedTab = (root.selectedTab - 1 + root.tabButtonList.length) % root.tabButtonList.length
}
event.accepted = true;
}
}
ColumnLayout {
anchors.margins: 5
NotificationList {
anchors.fill: parent
spacing: 0
PrimaryTabBar {
id: tabBar
tabButtonList: root.tabButtonList
externalTrackedTab: root.selectedTab
function onCurrentIndexChanged(currentIndex) {
root.selectedTab = currentIndex
}
}
SwipeView {
id: swipeView
Layout.topMargin: 5
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
currentIndex: root.selectedTab
onCurrentIndexChanged: {
tabBar.enableIndicatorAnimation = true
root.selectedTab = currentIndex
}
clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: swipeView.width
height: swipeView.height
radius: Appearance.rounding.small
}
}
NotificationList {}
VolumeMixer {}
}
anchors.margins: 5
}
}
}
@@ -13,6 +13,7 @@ import "./quickToggles/"
import "./quickToggles/classicStyle/"
import "./wifiNetworks/"
import "./bluetoothDevices/"
import "./volumeMixer/"
Item {
id: root
@@ -21,6 +22,8 @@ Item {
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
property bool showWifiDialog: false
property bool showBluetoothDialog: false
property bool showAudioOutputDialog: false
property bool showAudioInputDialog: false
property bool editMode: false
Connections {
@@ -29,6 +32,8 @@ Item {
if (!GlobalStates.sidebarRightOpen) {
root.showWifiDialog = false;
root.showBluetoothDialog = false;
root.showAudioOutputDialog = false;
root.showAudioInputDialog = false;
}
}
}
@@ -102,53 +107,71 @@ Item {
}
}
onShowWifiDialogChanged: if (showWifiDialog) wifiDialogLoader.active = true;
Loader {
ToggleDialog {
id: wifiDialogLoader
anchors.fill: parent
active: root.showWifiDialog || item.visible
onActiveChanged: {
if (active) {
item.show = true;
item.forceActiveFocus();
}
}
sourceComponent: WifiDialog {
onDismiss: {
show = false
root.showWifiDialog = false
}
onVisibleChanged: {
if (!visible && !root.showWifiDialog) wifiDialogLoader.active = false;
}
shownPropertyString: "showWifiDialog"
dialog: WifiDialog {}
onShownChanged: {
if (!shown) return;
Network.enableWifi();
Network.rescanWifi();
}
}
onShowBluetoothDialogChanged: {
if (showBluetoothDialog) bluetoothDialogLoader.active = true;
else Bluetooth.defaultAdapter.discovering = false;
}
Loader {
ToggleDialog {
id: bluetoothDialogLoader
shownPropertyString: "showBluetoothDialog"
dialog: BluetoothDialog {}
onShownChanged: {
if (!shown) {
Bluetooth.defaultAdapter.discovering = false;
} else {
Bluetooth.defaultAdapter.enabled = true;
Bluetooth.defaultAdapter.discovering = true;
}
}
}
ToggleDialog {
id: audioOutputDialogLoader
shownPropertyString: "showAudioOutputDialog"
dialog: VolumeDialog {
isSink: true
}
}
ToggleDialog {
id: audioInputDialogLoader
shownPropertyString: "showAudioInputDialog"
dialog: VolumeDialog {
isSink: false
}
}
component ToggleDialog: Loader {
id: toggleDialogLoader
required property string shownPropertyString
property alias dialog: toggleDialogLoader.sourceComponent
readonly property bool shown: root[shownPropertyString]
anchors.fill: parent
active: root.showBluetoothDialog || item.visible
onShownChanged: if (shown) toggleDialogLoader.active = true;
active: shown
onActiveChanged: {
if (active) {
item.show = true;
item.forceActiveFocus();
}
}
sourceComponent: BluetoothDialog {
onDismiss: {
show = false
root.showBluetoothDialog = false
Connections {
target: toggleDialogLoader.item
function onDismiss() {
toggleDialogLoader.item.show = false
root[toggleDialogLoader.shownPropertyString] = false;
}
onVisibleChanged: {
if (!visible && !root.showBluetoothDialog) bluetoothDialogLoader.active = false;
function onVisibleChanged() {
if (!toggleDialogLoader.item.visible && !root[toggleDialogLoader.shownPropertyString]) toggleDialogLoader.active = false;
}
}
}
@@ -163,15 +186,17 @@ Item {
Connections {
target: quickPanelImplLoader.item
function onOpenWifiDialog() {
Network.enableWifi();
Network.rescanWifi();
root.showWifiDialog = true;
}
function onOpenBluetoothDialog() {
Bluetooth.defaultAdapter.enabled = true;
Bluetooth.defaultAdapter.discovering = true;
root.showBluetoothDialog = true;
}
function onOpenAudioOutputDialog() {
root.showAudioOutputDialog = true;
}
function onOpenAudioInputDialog() {
root.showAudioInputDialog = true;
}
}
}
@@ -9,4 +9,6 @@ Rectangle {
signal openWifiDialog()
signal openBluetoothDialog()
signal openAudioOutputDialog()
signal openAudioInputDialog()
}
@@ -101,6 +101,8 @@ AbstractQuickPanel {
spacing: root.spacing
onOpenWifiDialog: root.openWifiDialog()
onOpenBluetoothDialog: root.openBluetoothDialog()
onOpenAudioOutputDialog: root.openAudioOutputDialog()
onOpenAudioInputDialog: root.openAudioInputDialog()
}
}
}
@@ -8,7 +8,7 @@ import Quickshell
AndroidQuickToggleButton {
id: root
name: Translation.tr("Audio")
name: Translation.tr("Audio output")
statusText: toggled ? Translation.tr("Unmuted") : Translation.tr("Muted")
toggled: !Audio.sink?.audio?.muted
buttonIcon: Audio.sink?.audio?.muted ? "volume_off" : "volume_up"
@@ -16,7 +16,11 @@ AndroidQuickToggleButton {
Audio.sink.audio.muted = !Audio.sink.audio.muted
}
altAction: () => {
root.openMenu()
}
StyledToolTip {
text: Translation.tr("Audio")
text: Translation.tr("Audio output | Right-click for volume mixer & device selector")
}
}
@@ -17,11 +17,14 @@ AndroidQuickToggleButton {
onClicked: {
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter?.enabled
}
altAction: () => {
root.openMenu()
}
StyledToolTip {
text: Translation.tr("%1 | Right-click to configure").arg(
(BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Bluetooth"))
+ (BluetoothStatus.activeDeviceCount > 1 ? ` +${BluetoothStatus.activeDeviceCount - 1}` : "")
)
)
}
}
@@ -8,7 +8,7 @@ import Quickshell
AndroidQuickToggleButton {
id: root
name: Translation.tr("Microphone")
name: Translation.tr("Audio input")
statusText: toggled ? Translation.tr("Enabled") : Translation.tr("Muted")
toggled: !Audio.source?.audio?.muted
buttonIcon: Audio.source?.audio?.muted ? "mic_off" : "mic"
@@ -16,7 +16,11 @@ AndroidQuickToggleButton {
Audio.source.audio.muted = !Audio.source.audio.muted
}
altAction: () => {
root.openMenu()
}
StyledToolTip {
text: Translation.tr("Microphone")
text: Translation.tr("Audio input | Right-click for volume mixer & device selector")
}
}
@@ -7,8 +7,6 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import "./androidStyle/"
DelegateChooser {
id: root
property bool editMode: false
@@ -18,6 +16,8 @@ DelegateChooser {
required property int startingIndex
signal openWifiDialog()
signal openBluetoothDialog()
signal openAudioOutputDialog()
signal openAudioInputDialog()
role: "type"
@@ -32,7 +32,7 @@ DelegateChooser {
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
altAction: () => {
onOpenMenu: {
root.openWifiDialog()
}
} }
@@ -48,7 +48,7 @@ DelegateChooser {
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
altAction: () => {
onOpenMenu: {
root.openBluetoothDialog()
}
} }
@@ -181,6 +181,9 @@ DelegateChooser {
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
onOpenMenu: {
root.openAudioInputDialog()
}
} }
DelegateChoice { roleValue: "audio"; AndroidAudioToggle {
@@ -194,6 +197,9 @@ DelegateChooser {
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
onOpenMenu: {
root.openAudioOutputDialog()
}
} }
DelegateChoice { roleValue: "notifications"; AndroidNotificationToggle {
@@ -0,0 +1,136 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
WindowDialog {
id: root
property bool isSink: true
function correctType(node) {
return (node.isSink === root.isSink) && node.audio
}
readonly property list<var> appPwNodes: Pipewire.nodes.values.filter((node) => { // Should be list<PwNode> but it breaks ScriptModel
return root.correctType(node) && node.isStream
})
readonly property bool hasApps: appPwNodes.length > 0
backgroundHeight: 700
WindowDialogTitle {
text: root.isSink ? Translation.tr("Audio output") : Translation.tr("Audio input")
}
WindowDialogSectionHeader {
visible: root.hasApps
text: Translation.tr("Applications")
}
WindowDialogSeparator {
visible: root.hasApps
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
DialogSectionListView {
visible: root.hasApps
Layout.fillHeight: true
model: ScriptModel {
values: root.appPwNodes
}
delegate: VolumeMixerEntry {
anchors {
left: parent?.left
right: parent?.right
}
required property var modelData
node: modelData
}
}
WindowDialogSectionHeader {
text: Translation.tr("Devices")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
DialogSectionListView {
Layout.fillHeight: !root.hasApps
Layout.preferredHeight: 180
model: ScriptModel {
values: Pipewire.nodes.values.filter(node => {
return root.correctType(node) && !node.isStream
})
}
delegate: StyledRadioButton {
id: radioButton
required property var modelData
anchors {
left: parent?.left
right: parent?.right
}
description: modelData.description
checked: modelData.id === (root.isSink ? Pipewire.preferredDefaultAudioSink?.id : Pipewire.preferredDefaultAudioSource?.id)
onCheckedChanged: {
if (!checked) return;
if (root.isSink) {
Pipewire.preferredDefaultAudioSink = modelData
} else {
Pipewire.preferredDefaultAudioSource = modelData
}
}
}
}
WindowDialogSeparator {
Layout.leftMargin: 0
Layout.rightMargin: 0
}
WindowDialogButtonRow {
DialogButton {
buttonText: Translation.tr("Details")
onClicked: {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.volumeMixer}`]);
GlobalStates.sidebarRightOpen = false;
}
}
Item {
Layout.fillWidth: true
}
DialogButton {
buttonText: Translation.tr("Done")
onClicked: root.dismiss()
}
}
component DialogSectionListView: StyledListView {
Layout.fillWidth: true
Layout.topMargin: -22
Layout.bottomMargin: -16
Layout.leftMargin: -Appearance.rounding.large
Layout.rightMargin: -Appearance.rounding.large
topMargin: 12
bottomMargin: 12
leftMargin: 20
rightMargin: 20
clip: true
spacing: 4
animateAppearance: false
}
}
@@ -1,275 +0,0 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
Item {
id: root
property bool showDeviceSelector: false
property bool deviceSelectorInput
property int dialogMargins: 16
property PwNode selectedDevice
readonly property list<PwNode> appPwNodes: Pipewire.nodes.values.filter((node) => {
// return node.type == "21" // Alternative, not as clean
return node.isSink && node.isStream
})
function showDeviceSelectorDialog(input: bool) {
root.selectedDevice = null
root.showDeviceSelector = true
root.deviceSelectorInput = input
}
Keys.onPressed: (event) => {
// Close dialog on pressing Esc if open
if (event.key === Qt.Key_Escape && root.showDeviceSelector) {
root.showDeviceSelector = false
event.accepted = true;
}
}
ColumnLayout {
anchors.fill: parent
Item {
Layout.fillWidth: true
Layout.fillHeight: true
StyledListView {
id: listView
model: root.appPwNodes
clip: true
anchors {
fill: parent
topMargin: 10
bottomMargin: 10
}
spacing: 6
delegate: VolumeMixerEntry {
// Layout.fillWidth: true
anchors {
left: parent.left
right: parent.right
leftMargin: 10
rightMargin: 10
}
required property var modelData
node: modelData
}
}
// Placeholder when list is empty
Item {
anchors.fill: listView
visible: opacity > 0
opacity: (root.appPwNodes.length === 0) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.menuDecel.duration
easing.type: Appearance.animation.menuDecel.type
}
}
ColumnLayout {
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignHCenter
iconSize: 55
color: Appearance.m3colors.m3outline
text: "brand_awareness"
}
StyledText {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3outline
horizontalAlignment: Text.AlignHCenter
text: Translation.tr("No audio source")
}
}
}
}
// Device selector
RowLayout {
id: deviceSelectorRowLayout
Layout.fillWidth: true
Layout.fillHeight: false
uniformCellSizes: true
AudioDeviceSelectorButton {
Layout.fillWidth: true
input: false
downAction: () => root.showDeviceSelectorDialog(input)
}
AudioDeviceSelectorButton {
Layout.fillWidth: true
input: true
downAction: () => root.showDeviceSelectorDialog(input)
}
}
}
// Device selector dialog
Item {
anchors.fill: parent
z: 9999
visible: opacity > 0
opacity: root.showDeviceSelector ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
Rectangle { // Scrim
id: scrimOverlay
anchors.fill: parent
radius: Appearance.rounding.small
color: Appearance.colors.colScrim
MouseArea {
hoverEnabled: true
anchors.fill: parent
preventStealing: true
propagateComposedEvents: false
}
}
Rectangle { // The dialog
id: dialog
color: Appearance.colors.colSurfaceContainerHigh
radius: Appearance.rounding.normal
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 30
implicitHeight: dialogColumnLayout.implicitHeight
ColumnLayout {
id: dialogColumnLayout
anchors.fill: parent
spacing: 16
StyledText {
id: dialogTitle
Layout.topMargin: dialogMargins
Layout.leftMargin: dialogMargins
Layout.rightMargin: dialogMargins
Layout.alignment: Qt.AlignLeft
color: Appearance.m3colors.m3onSurface
font.pixelSize: Appearance.font.pixelSize.larger
text: root.deviceSelectorInput ? Translation.tr("Select input device") : Translation.tr("Select output device")
}
Rectangle {
color: Appearance.m3colors.m3outline
implicitHeight: 1
Layout.fillWidth: true
Layout.leftMargin: dialogMargins
Layout.rightMargin: dialogMargins
}
StyledFlickable {
id: dialogFlickable
Layout.fillWidth: true
clip: true
implicitHeight: Math.min(scrimOverlay.height - dialogMargins * 8 - dialogTitle.height - dialogButtonsRowLayout.height, devicesColumnLayout.implicitHeight)
contentHeight: devicesColumnLayout.implicitHeight
ColumnLayout {
id: devicesColumnLayout
anchors.fill: parent
Layout.fillWidth: true
spacing: 0
Repeater {
model: ScriptModel {
values: Pipewire.nodes.values.filter(node => {
return !node.isStream && node.isSink !== root.deviceSelectorInput && node.audio
})
}
// This could and should be refractored, but all data becomes null when passed wtf
delegate: StyledRadioButton {
id: radioButton
required property var modelData
Layout.leftMargin: root.dialogMargins
Layout.rightMargin: root.dialogMargins
Layout.fillWidth: true
description: modelData.description
checked: modelData.id === Pipewire.defaultAudioSink?.id
Connections {
target: root
function onShowDeviceSelectorChanged() {
if(!root.showDeviceSelector) return;
radioButton.checked = (modelData.id === Pipewire.defaultAudioSink?.id)
}
}
onCheckedChanged: {
if (checked) {
root.selectedDevice = modelData
}
}
}
}
Item {
implicitHeight: dialogMargins
}
}
}
Rectangle {
color: Appearance.m3colors.m3outline
implicitHeight: 1
Layout.fillWidth: true
Layout.leftMargin: dialogMargins
Layout.rightMargin: dialogMargins
}
RowLayout {
id: dialogButtonsRowLayout
Layout.bottomMargin: dialogMargins
Layout.leftMargin: dialogMargins
Layout.rightMargin: dialogMargins
Layout.alignment: Qt.AlignRight
DialogButton {
buttonText: Translation.tr("Cancel")
onClicked: {
root.showDeviceSelector = false
}
}
DialogButton {
buttonText: Translation.tr("OK")
onClicked: {
root.showDeviceSelector = false
if (root.selectedDevice) {
if (root.deviceSelectorInput) {
Pipewire.preferredDefaultAudioSource = root.selectedDevice
} else {
Pipewire.preferredDefaultAudioSink = root.selectedDevice
}
}
}
}
}
}
}
}
}
@@ -21,7 +21,7 @@ Item {
spacing: 6
Image {
property real size: slider.height * 0.9
property real size: 36
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
visible: source != ""
sourceSize.width: size
@@ -57,6 +57,7 @@ Item {
id: slider
value: root.node.audio.volume
onMoved: root.node.audio.volume = value
configuration: StyledSlider.Configuration.S
}
}
}