waffles: action center: volume menu

This commit is contained in:
end-4
2025-11-19 23:39:18 +01:00
parent bca177eed2
commit 5c8d824749
42 changed files with 680 additions and 129 deletions
@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M10.295 19.716a1 1 0 0 0 1.404-1.425l-5.37-5.29h13.67a1 1 0 1 0 0-2H6.336L11.7 5.714a1 1 0 0 0-1.404-1.424l-6.924 6.822a1.25 1.25 0 0 0 0 1.78l6.924 6.823Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 283 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M10.733 19.79a.75.75 0 0 0 1.034-1.086L5.516 12.75H20.25a.75.75 0 0 0 0-1.5H5.516l6.251-5.955a.75.75 0 0 0-1.034-1.086l-7.42 7.067a.995.995 0 0 0-.3.58.754.754 0 0 0 .001.289.995.995 0 0 0 .3.579l7.419 7.067Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 336 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6 21.25c0 .415.336.75.75.75h8.931a1.747 1.747 0 0 1-.708-.464L14 20.496v.004h-4l-.001-2.498H11V16a1 1 0 0 1 1-1h1.533l1.44-1.536c.862-.919 2.264-.624 2.804.331.366-.244.81-.34 1.238-.275a1.75 1.75 0 0 1 2.855-1.114c.042.035.085.073.129.113V5.25l-.005-.154A2.25 2.25 0 0 0 19.749 3H4.25l-.154.005A2.25 2.25 0 0 0 2 5.25v10.502l.005.154a2.25 2.25 0 0 0 2.245 2.096h4.249V20.5H6.75l-.102.007A.75.75 0 0 0 6 21.25Z" fill="#212121"/><path d="M22.143 14.302c-.328-.547-.665-.92-.913-1.128a.75.75 0 0 0-.96 1.153c.127.105.353.356.587.747.401.669.643 1.475.643 2.426 0 .952-.242 1.758-.643 2.427-.234.39-.46.642-.587.747a.75.75 0 0 0 .96 1.153c.248-.207.585-.581.913-1.128.536-.894.857-1.963.857-3.199 0-1.235-.32-2.304-.857-3.198Z" fill="#212121"/><path d="M19.874 15.397a3.075 3.075 0 0 0-.674-.747.75.75 0 0 0-.9 1.2c.062.047.19.175.326.379.234.351.374.771.374 1.271 0 .5-.14.921-.374 1.272a1.68 1.68 0 0 1-.326.379l-.084.073a.75.75 0 0 0 .984 1.127c.189-.142.435-.388.674-.747A3.734 3.734 0 0 0 20.5 17.5c0-.812-.235-1.517-.626-2.103ZM17 14.75a.75.75 0 0 0-1.314-.493L14.16 16h-1.41a.75.75 0 0 0-.75.75v1.5c0 .415.336.75.75.75h1.41l1.526 1.744A.75.75 0 0 0 17 20.25v-5.5Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6 21.25c0 .415.336.75.75.75h8.931a1.747 1.747 0 0 1-.708-.464L14 20.496v.004h-4l-.001-2.498H11v-1.5H4.25l-.102-.007a.75.75 0 0 1-.648-.743V5.25l.007-.102A.75.75 0 0 1 4.25 4.5h15.499l.102.007a.75.75 0 0 1 .648.743v6.769c.474-.069.974.056 1.371.387.042.035.085.073.129.113V5.25l-.005-.154A2.25 2.25 0 0 0 19.749 3H4.25l-.154.005A2.25 2.25 0 0 0 2 5.25v10.502l.005.154a2.25 2.25 0 0 0 2.245 2.096h4.249V20.5H6.75l-.102.007A.75.75 0 0 0 6 21.25Z" fill="#212121"/><path d="M22.143 14.302c-.328-.547-.665-.92-.913-1.128a.75.75 0 0 0-.96 1.153c.127.105.353.356.587.747.401.669.643 1.475.643 2.426 0 .952-.242 1.758-.643 2.427-.234.39-.46.642-.587.747a.75.75 0 0 0 .96 1.153c.248-.207.585-.581.913-1.128.536-.894.857-1.963.857-3.199 0-1.235-.32-2.304-.857-3.198Z" fill="#212121"/><path d="M19.874 15.397a3.075 3.075 0 0 0-.674-.747.75.75 0 0 0-.9 1.2c.062.047.19.175.326.379.234.351.374.771.374 1.271 0 .5-.14.921-.374 1.272a1.68 1.68 0 0 1-.326.379l-.084.073a.75.75 0 0 0 .984 1.127c.189-.142.435-.388.674-.747A3.734 3.734 0 0 0 20.5 17.5c0-.812-.235-1.517-.626-2.103ZM17 14.75a.75.75 0 0 0-1.314-.493L14.16 16h-1.41a.75.75 0 0 0-.75.75v1.5c0 .415.336.75.75.75h1.41l1.526 1.744A.75.75 0 0 0 17 20.25v-5.5Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c5.523 0 10 4.477 10 10v7a3 3 0 0 1-3 3h-3a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h4.5v-2a8.5 8.5 0 0 0-17 0v2H8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H5a3 3 0 0 1-3-3v-7C2 6.477 6.477 2 12 2Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 307 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M22 12v7a3 3 0 0 1-2.824 2.995L19 22h-3a1 1 0 0 1-.993-.883L15 21v-6a1 1 0 0 1 .883-.993L16 14h4.5v-2a8.5 8.5 0 0 0-17 0v2H8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H5a3 3 0 0 1-3-3v-7C2 6.477 6.477 2 12 2s10 4.477 10 10v7-7ZM7.5 15.5h-4V19A1.5 1.5 0 0 0 5 20.5h2.5v-5Zm13 0h-4v5H19a1.5 1.5 0 0 0 1.493-1.355L20.5 19v-3.5Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 439 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M15 4.25c0-1.079-1.274-1.65-2.08-.934L8.427 7.309a.75.75 0 0 1-.498.19H4.25A2.25 2.25 0 0 0 2 9.749v4.497a2.25 2.25 0 0 0 2.25 2.25h3.68a.75.75 0 0 1 .498.19l4.491 3.994c.806.716 2.081.144 2.081-.934V4.25ZM16.22 9.22a.75.75 0 0 1 1.06 0L19 10.94l1.72-1.72a.75.75 0 1 1 1.06 1.06L20.06 12l1.72 1.72a.75.75 0 1 1-1.06 1.06L19 13.06l-1.72 1.72a.75.75 0 1 1-1.06-1.06L17.94 12l-1.72-1.72a.75.75 0 0 1 0-1.06Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 532 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.92 3.316c.806-.717 2.08-.145 2.08.934v15.496c0 1.078-1.274 1.65-2.08.934l-4.492-3.994a.75.75 0 0 0-.498-.19H4.25A2.25 2.25 0 0 1 2 14.247V9.75a2.25 2.25 0 0 1 2.25-2.25h3.68a.75.75 0 0 0 .498-.19l4.491-3.993Zm.58 1.49L9.425 8.43A2.25 2.25 0 0 1 7.93 9H4.25a.75.75 0 0 0-.75.75v4.497c0 .415.336.75.75.75h3.68a2.25 2.25 0 0 1 1.495.57l4.075 3.623V4.807ZM16.22 9.22a.75.75 0 0 1 1.06 0L19 10.94l1.72-1.72a.75.75 0 1 1 1.06 1.06L20.06 12l1.72 1.72a.75.75 0 1 1-1.06 1.06L19 13.06l-1.72 1.72a.75.75 0 1 1-1.06-1.06L17.94 12l-1.72-1.72a.75.75 0 0 1 0-1.06Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 682 B

@@ -1,12 +1,12 @@
import QtQuick
import qs.modules.common
import qs.modules.common.widgets
Loader {
id: root
property bool shown: true
property alias fade: opacityBehavior.enabled
property alias animation: opacityBehavior.animation
opacity: shown ? 1 : 0
visible: opacity > 0
active: opacity > 0
@@ -13,7 +13,9 @@ Item {
property bool alternativeVisibleCondition: false
property real horizontalPadding: 10
property real verticalPadding: 5
property real horizontalMargin: horizontalPadding
property real verticalMargin: verticalPadding
function updateAnchor() {
tooltipLoader.item?.anchor.updateAnchor();
}
@@ -49,8 +51,8 @@ Item {
}
color: "transparent"
implicitWidth: root.contentItem.implicitWidth + root.horizontalPadding * 2
implicitHeight: root.contentItem.implicitHeight + root.verticalPadding * 2
implicitWidth: root.contentItem.implicitWidth + root.horizontalMargin * 2
implicitHeight: root.contentItem.implicitHeight + root.verticalMargin * 2
data: [root.contentItem]
}
@@ -10,15 +10,8 @@ import Quickshell.Services.Pipewire
ColumnLayout {
id: root
required property bool isSink
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 list<var> devices: Pipewire.nodes.values.filter(node => {
return root.correctType(node) && !node.isStream
})
readonly property list<var> appPwNodes: isSink ? Audio.outputAppNodes : Audio.inputAppNodes
readonly property list<var> devices: isSink ? Audio.outputDevices : Audio.inputDevices
readonly property bool hasApps: appPwNodes.length > 0
spacing: 16
@@ -44,21 +37,21 @@ ColumnLayout {
Layout.fillHeight: false
Layout.fillWidth: true
Layout.bottomMargin: 6
model: root.devices.map(node => (node.nickname || node.description || Translation.tr("Unknown")))
model: root.devices.map(node => Audio.friendlyDeviceName(node))
currentIndex: root.devices.findIndex(item => {
if (root.isSink) {
return item.id === Pipewire.preferredDefaultAudioSink?.id
return item.id === Pipewire.defaultAudioSink?.id
} else {
return item.id === Pipewire.preferredDefaultAudioSource?.id
return item.id === Pipewire.defaultAudioSource?.id
}
})
onActivated: (index) => {
print(index)
const item = root.devices[index]
if (root.isSink) {
Pipewire.preferredDefaultAudioSink = item
Audio.setDefaultSink(item)
} else {
Pipewire.preferredDefaultAudioSource = item
Audio.setDefaultSource(item)
}
}
}
@@ -10,7 +10,7 @@ Item {
id: root
required property PwNode node
PwObjectTracker {
objects: [node]
objects: [root.node]
}
implicitHeight: rowLayout.implicitHeight
@@ -47,7 +47,7 @@ Item {
elide: Text.ElideRight
text: {
// application.name -> description -> name
const app = root.node?.properties["application.name"] ?? (root.node.description != "" ? root.node.description : root.node.name);
const app = Audio.appNodeDisplayName(root.node);
const media = root.node.properties["media.name"];
return media != undefined ? `${app} ${media}` : app;
}
@@ -7,32 +7,34 @@ import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter.mainPage
WBarAttachedPanelContent {
id: root
contentItem: StackView {
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
contentItem: StackView { // TODO: Make this a WStackView with proper anim
id: stackView
anchors.fill: parent
implicitWidth: initItem.implicitWidth
implicitHeight: initItem.implicitHeight
initialItem: ColumnLayout {
anchors.centerIn: parent
spacing: 0
ActionCenterBody {}
Rectangle {
Layout.fillHeight: false
Layout.fillWidth: true
color: Looks.colors.bgPanelSeparator
implicitHeight: 1
}
ActionCenterFooter {}
initialItem: PageColumn {
id: initItem
MainPageBody {}
Separator {}
MainPageFooter {}
}
Component.onCompleted: {
ActionCenterContext.stackView = this
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.BackButton
onClicked: {
ActionCenterContext.back()
}
}
}
}
@@ -9,4 +9,17 @@ Singleton {
property StackView stackView
function push(component) {
if (stackView) {
item = stackView.push(component)
stackView.implicitWidth = item.implicitWidth
stackView.implicitHeight = item.implicitHeight
}
}
function back() {
if (stackView && stackView.depth > 1) {
stackView.pop()
}
}
}
@@ -0,0 +1,14 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
Rectangle {
Layout.fillHeight: true
Layout.fillWidth: true
color: Looks.colors.bgPanelBody
}
@@ -0,0 +1,17 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
Rectangle {
Layout.fillHeight: false
Layout.fillWidth: true
color: Looks.colors.bgPanelFooter
implicitWidth: 360
implicitHeight: 47
}
@@ -0,0 +1,25 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
RowLayout {
id: root
required property string title
spacing: 4
WPanelIconButton {
iconName: "arrow-left"
onClicked: ActionCenterContext.back()
}
WText {
id: titleText
Layout.fillWidth: true
elide: Text.ElideRight
text: root.title
font.pixelSize: Looks.font.pixelSize.large
}
}
@@ -0,0 +1,6 @@
import QtQuick
import QtQuick.Layouts
ColumnLayout {
spacing: 0
}
@@ -0,0 +1,20 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
WText {
Layout.leftMargin: 12
Layout.rightMargin: 12
Layout.topMargin: 6
Layout.bottomMargin: 6
font {
weight: Looks.font.weight.stronger
pixelSize: Looks.font.pixelSize.large
}
}
@@ -0,0 +1,10 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.waffle.looks
Rectangle {
Layout.fillHeight: false
Layout.fillWidth: true
color: Looks.colors.bgPanelSeparator
implicitHeight: 1
}
@@ -54,7 +54,8 @@ Scope {
ActionCenterContent {
id: content
anchors.centerIn: parent
anchors.fill: parent
anchors.margins: visualMargin
focus: true
Keys.onPressed: event => { // Esc to close
@@ -6,15 +6,10 @@ import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
Rectangle {
BodyRectangle {
id: root
Layout.fillHeight: true
Layout.fillWidth: true
color: Looks.colors.bgPanelBody
implicitWidth: 360
implicitHeight: contentLayout.implicitHeight
ColumnLayout {
@@ -22,7 +17,7 @@ Rectangle {
anchors.fill: parent
spacing: 0
ActionCenterBodyToggles {
MainPageBodyToggles {
id: togglesContainer
Layout.fillWidth: true
}
@@ -33,7 +28,7 @@ Rectangle {
color: Looks.colors.bg1Border
}
ActionCenterBodySliders {
MainPageBodySliders {
Layout.margins: 12
Layout.topMargin: 18
Layout.bottomMargin: 14
@@ -6,6 +6,8 @@ import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
import qs.modules.waffle.actionCenter.volumeControl
ColumnLayout {
id: root
@@ -58,6 +60,13 @@ ColumnLayout {
}
WPanelIconButton {
Component {
id: volumeControlComp
VolumeControl {}
}
onClicked: {
ActionCenterContext.push(volumeControlComp)
}
contentItem: Item {
anchors.centerIn: parent
Row {
@@ -6,14 +6,9 @@ import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
Rectangle {
Layout.fillHeight: false
Layout.fillWidth: true
color: Looks.colors.bgPanelFooter
implicitWidth: 360
implicitHeight: 47
FooterRectangle {
// Battery button
WPanelFooterButton {
@@ -0,0 +1,160 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
Rectangle {
id: root
implicitWidth: 360
implicitHeight: 352
PageColumn {
anchors.fill: parent
BodyRectangle {
implicitHeight: 400
implicitWidth: 50
ColumnLayout {
anchors.fill: parent
anchors.margins: 4
spacing: 4
HeaderRow {
Layout.fillWidth: true
title: qsTr("Sound output")
}
StyledFlickable {
id: flickable
Layout.fillHeight: true
Layout.fillWidth: true
contentHeight: contentLayout.implicitHeight
contentWidth: width
clip: true
AudioChoices {
id: contentLayout
width: flickable.width
}
}
}
}
Separator {}
FooterRectangle {
WButton {
id: moreSettingsButton
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
}
inset: 0
implicitHeight: 40
implicitWidth: contentItem.implicitWidth + 30
color: "transparent"
onClicked: {
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "sidebarLeft", "toggle"]);
Quickshell.execDetached(["bash", "-c", Config.options.apps.volumeMixer]);
}
contentItem: Item {
anchors.centerIn: parent
implicitWidth: buttonText.implicitWidth
WText {
id: buttonText
anchors.centerIn: parent
text: qsTr("More volume settings")
color: moreSettingsButton.pressed ? Looks.colors.fg : Looks.colors.fg1
}
}
}
}
}
component AudioChoices: ColumnLayout {
spacing: 4
SectionText {
text: qsTr("Output device")
}
Repeater {
model: ScriptModel {
values: Audio.outputDevices
}
delegate: WChoiceButton {
required property var modelData
icon.name: WIcons.audioDeviceIcon(modelData)
text: Audio.friendlyDeviceName(modelData)
checked: Audio.sink === modelData
onClicked: {
Audio.setDefaultSink(modelData);
}
}
}
Separator {
visible: EasyEffects.available
color: Looks.colors.bg2Hover
}
////////////////////////////////////////////////////////////
SectionText {
visible: EasyEffects.available
text: qsTr("Sound effects")
}
WChoiceButton {
visible: EasyEffects.available
text: Translation.tr("Off")
checked: !EasyEffects.active
onClicked: EasyEffects.disable()
}
WChoiceButton {
visible: EasyEffects.available
text: "EasyEffects"
checked: EasyEffects.active
onClicked: EasyEffects.enable()
}
Separator {
color: Looks.colors.bg2Hover
}
////////////////////////////////////////////////////////////
SectionText {
visible: EasyEffects.available
text: qsTr("Volume mixer")
}
VolumeEntry {
node: Audio.sink
icon: "speaker"
monochrome: true
}
Repeater {
model: ScriptModel {
values: Audio.outputAppNodes
}
delegate: VolumeEntry {
required property var modelData
node: modelData
}
}
}
}
@@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.actionCenter
RowLayout {
id: root
required property PwNode node
property alias icon: iconButton.iconName
property alias monochrome: iconButton.monochrome
monochrome: false
PwObjectTracker { // Necessary for useful info to be present in 'node'
objects: [root.node]
}
WPanelIconButton {
id: iconButton
iconName: WIcons.audioAppIcon(root.node)
onClicked: root.node.audio.muted = !root.node?.audio.muted
FluentIcon {
id: muteIcon
visible: root.node?.audio.muted ?? false
anchors {
bottom: parent.bottom
right: parent.right
margins: -1
}
implicitSize: 14
icon: "speaker-mute"
}
WToolTip {
extraVisibleCondition: iconButton.shouldShowTooltip
text: Audio.appNodeDisplayName(root.node)
}
}
WSlider {
Layout.fillWidth: true
Layout.rightMargin: 10
value: root.node?.audio.volume ?? 0
onMoved: root.node.audio.volume = value
}
}
@@ -5,41 +5,21 @@ import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
Button {
WButton {
id: root
property var altAction: () => {}
property var middleClickAction: () => {}
property color colBackground: ColorUtils.transparentize(Looks.colors.bg1)
property color colBackgroundHover: Looks.colors.bg1Hover
property color colBackgroundActive: Looks.colors.bg1Active
colBackground: ColorUtils.transparentize(Looks.colors.bg1)
colBackgroundHover: Looks.colors.bg1Hover
colBackgroundActive: Looks.colors.bg1Active
property color colBackgroundBorder
property color color
Layout.fillHeight: true
topInset: 4
bottomInset: 4
signal hoverTimedOut()
property bool shouldShowTooltip: false
property Timer hoverTimer: Timer {
id: hoverTimer
running: root.hovered
interval: 400
onTriggered: {
root.hoverTimedOut()
}
}
onHoverTimedOut: {
root.shouldShowTooltip = true
}
onHoveredChanged: {
if (!root.hovered) {
root.shouldShowTooltip = false
root.hoverTimer.stop()
}
}
colBackgroundBorder: ColorUtils.transparentize(Looks.colors.bg1Border, (root.checked || root.hovered) ? Looks.contentTransparency : 1)
color: {
if (root.down) {
@@ -37,6 +37,7 @@ BarButton {
}
}
FluentIcon {
visible: Notifications.silent
anchors.verticalCenter: parent.verticalCenter
icon: "alert-snooze"
implicitSize: 18
@@ -105,8 +105,8 @@ RowLayout {
BarToolTip {
id: pinTooltip
extraVisibleCondition: trayButton.Drag.active && pinDropArea.containsDrag && pinDropArea.willPin
realContentHorizontalPadding: 6
realContentVerticalPadding: 6
horizontalPadding: 6
verticalPadding: 6
realContentItem: FluentIcon {
anchors.centerIn: parent
icon: "pin-off"
@@ -76,7 +76,8 @@ Singleton {
}
property QtObject pixelSize: QtObject {
property real normal: 11
property real large: 14
property real large: 13
property real larger: 15
}
}
@@ -78,11 +78,14 @@ Item {
}
}
Item {
Rectangle {
id: contentArea
color: "red"
z: 0
anchors.fill: borderRect
anchors.margins: borderRect.border.width
implicitWidth: contentItem.implicitWidth
implicitHeight: contentItem.implicitHeight
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
@@ -15,6 +15,7 @@ Button {
property color colBackgroundToggled: Looks.colors.accent
property color colBackgroundToggledHover: Looks.colors.accentHover
property color colBackgroundToggledActive: Looks.colors.accentActive
property color colForeground: Looks.colors.fg
property alias backgroundOpacity: backgroundRect.opacity
property color color: {
if (root.checked) {
@@ -35,7 +36,29 @@ Button {
}
}
// Hover stuff
signal hoverTimedOut()
property bool shouldShowTooltip: false
property Timer hoverTimer: Timer {
id: hoverTimer
running: root.hovered
interval: 400
onTriggered: {
root.hoverTimedOut()
}
}
onHoverTimedOut: {
root.shouldShowTooltip = true
}
onHoveredChanged: {
if (!root.hovered) {
root.shouldShowTooltip = false
root.hoverTimer.stop()
}
}
property alias monochromeIcon: buttonIcon.monochrome
property alias buttonSpacing: contentLayout.spacing
property bool forceShowIcon: false
property var altAction: () => {}
@@ -93,16 +116,16 @@ Button {
spacing: 12
FluentIcon {
id: buttonIcon
visible: root.icon.name !== "" || root.forceShowIcon
monochrome: true
implicitSize: 16
Layout.leftMargin: 6
Layout.leftMargin: root.iconLeftMargin
Layout.fillWidth: false
Layout.alignment: Qt.AlignVCenter
icon: root.icon.name
color: root.colForeground
visible: root.icon.name !== ""
}
WText {
Layout.rightMargin: 12
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
text: root.text
@@ -110,6 +133,7 @@ Button {
font {
pixelSize: Looks.font.pixelSize.large
}
color: root.colForeground
}
}
}
@@ -0,0 +1,67 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
WButton {
id: root
Layout.fillWidth: true
implicitWidth: contentItem.implicitWidth
horizontalPadding: 10
verticalPadding: 11
inset: 0
buttonSpacing: 8
property color color: {
if (root.checked) {
if (root.down) {
return root.colBackgroundHover;
} else if (root.hovered && !root.down) {
return root.colBackgroundActive;
} else {
return root.colBackgroundHover;
}
}
if (root.down) {
return root.colBackgroundActive;
} else if (root.hovered && !root.down) {
return root.colBackgroundHover;
} else {
return root.colBackground;
}
}
background: Rectangle {
id: backgroundRect
radius: Looks.radius.medium
color: root.color
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
WFadeLoader {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
shown: root.checked
sourceComponent: Rectangle {
implicitWidth: 3
implicitHeight: 3
radius: width / 2
color: Looks.colors.accent
Component.onCompleted: {
implicitHeight = 16;
}
Behavior on implicitHeight {
animation: Looks.transition.opacity.createObject(this)
}
}
}
}
}
@@ -0,0 +1,19 @@
import QtQuick
import qs.modules.common
// Yes, this is (mostly) a copy of FadeLoader.
// The animation of a Behavior cannot be changed... I'd love to be proven wrong.
Loader {
id: root
property bool shown: true
property alias fade: opacityBehavior.enabled
property alias animation: opacityBehavior.animation
opacity: shown ? 1 : 0
visible: opacity > 0
active: opacity > 0
Behavior on opacity {
id: opacityBehavior
animation: Looks.transition.opacity.createObject(null)
}
}
@@ -8,24 +8,34 @@ Singleton {
id: root
property string internetIcon: {
if (Network.ethernet) return "ethernet";
if (Network.ethernet)
return "ethernet";
if (Network.wifiEnabled) {
const strength = Network.networkStrength;
if (strength > 75) return "wifi-1";
if (strength > 50) return "wifi-2";
if (strength > 25) return "wifi-3";
if (strength > 75)
return "wifi-1";
if (strength > 50)
return "wifi-2";
if (strength > 25)
return "wifi-3";
return "wifi-4";
}
if (Network.wifiStatus === "connecting") return "wifi-4";
if (Network.wifiStatus === "disconnected") return "wifi-off";
if (Network.wifiStatus === "disabled") return "wifi-off";
if (Network.wifiStatus === "connecting")
return "wifi-4";
if (Network.wifiStatus === "disconnected")
return "wifi-off";
if (Network.wifiStatus === "disabled")
return "wifi-off";
return "wifi-warning";
}
property string batteryIcon: {
if (Battery.isCharging) return "battery-charge";
if (Battery.isCriticalAndNotCharging) return "battery-warning";
if (Battery.percentage >= 0.9) return "battery-full";
if (Battery.isCharging)
return "battery-charge";
if (Battery.isCriticalAndNotCharging)
return "battery-warning";
if (Battery.percentage >= 0.9)
return "battery-full";
return `battery-${Math.ceil(Battery.percentage * 10)}`;
}
@@ -33,7 +43,7 @@ Singleton {
const muted = Audio.sink?.audio.muted ?? false;
const volume = Audio.sink?.audio.volume ?? 0;
if (muted)
return volume > 0 ? "speaker-off" : "speaker-none";
return "speaker-mute";
if (volume == 0)
return "speaker-none";
if (volume < 0.5)
@@ -53,10 +63,39 @@ Singleton {
property string notificationsIcon: Notifications.silent ? "alert-snooze" : "alert"
property string powerProfileIcon: {
switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: return "leaf-two";
case PowerProfile.Balanced: return "flash-on";
case PowerProfile.Performance: return "fire";
switch (PowerProfiles.profile) {
case PowerProfile.PowerSaver:
return "leaf-two";
case PowerProfile.Balanced:
return "flash-on";
case PowerProfile.Performance:
return "fire";
}
}
function audioDeviceIcon(node) {
if (!node.isSink)
return "mic-on";
const monitor = /monitor|hdmi/i;
const headphones = /headset|headphone|bluez|wireless/i;
const speakers = /speaker|output/i;
if (monitor.test(node.nickname) || monitor.test(node.description) || monitor.test(node.name)) {
return "desktop-speaker";
}
if (headphones.test(node.nickname) || headphones.test(node.description) || headphones.test(node.name)) {
return "headphones";
}
if (speakers.test(node.nickname) || speakers.test(node.description) || speakers.test(node.name)) {
return "speaker";
}
return "speaker";
}
function audioAppIcon(node) {
let icon;
icon = AppSearch.guessIcon(node?.properties["application.icon-name"] ?? "");
if (AppSearch.iconExists(icon)) return icon;
icon = AppSearch.guessIcon(node?.properties["node.name"] ?? "");
return icon;
}
}
@@ -10,6 +10,7 @@ WButton {
id: root
property alias iconName: iconContent.icon
property alias monochrome: iconContent.monochrome
inset: 0
implicitWidth: 40
implicitHeight: 40
@@ -10,37 +10,22 @@ import qs.modules.waffle.looks
PopupToolTip {
id: root
property Item realContentItem
required property Item realContentItem
realContentItem: WText {
text: root.text
anchors.centerIn: parent
}
property real visualMargin: 11
verticalPadding: visualMargin
horizontalPadding: visualMargin
property real realContentVerticalPadding: 8
property real realContentHorizontalPadding: 10
verticalPadding: 8
horizontalPadding: 10
verticalMargin: visualMargin
horizontalMargin: visualMargin
contentItem: Item {
anchors.centerIn: parent
implicitWidth: realContent.implicitWidth + 2 * 2
implicitHeight: realContent.implicitHeight + 2 * 2
WAmbientShadow {
target: realContent
}
Rectangle {
id: realContent
z: 1
anchors.centerIn: parent
implicitWidth: root.realContentItem.implicitWidth + root.realContentHorizontalPadding * 2
implicitHeight: root.realContentItem.implicitHeight + root.realContentVerticalPadding * 2
color: Looks.colors.bg1
radius: Looks.radius.medium
children: [root.realContentItem]
}
contentItem: WToolTipContent {
id: tooltipContent
realContentItem: root.realContentItem
horizontalPadding: root.horizontalPadding
verticalPadding: root.verticalPadding
}
}
@@ -10,8 +10,7 @@ Slider {
id: root
property real trackWidth: 4
// leftPadding: handle.width / 2
// rightPadding: handle.width / 2
property string tooltipContent: `${Math.round(value * 100)}`
leftPadding: 0
rightPadding: 0
@@ -77,5 +76,14 @@ Slider {
animation: Looks.transition.enter.createObject(this)
}
}
WToolTip {
id: tooltip
extraVisibleCondition: root.pressed
text: root.tooltipContent
font.pixelSize: Looks.font.pixelSize.larger
verticalPadding: 3
horizontalPadding: 8
}
}
}
@@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
StyledToolTip {
id: root
required property Item realContentItem
font {
family: Looks.font.family.ui
pixelSize: Looks.font.pixelSize.normal
weight: Looks.font.weight.regular
}
realContentItem: WText {
text: root.text
font: root.font
anchors.centerIn: parent
}
verticalPadding: 8
horizontalPadding: 10
contentItem: WToolTipContent {
id: tooltipContent
realContentItem: root.realContentItem
horizontalPadding: root.horizontalPadding
verticalPadding: root.verticalPadding
}
}
@@ -0,0 +1,29 @@
import QtQuick
import Quickshell
import qs.modules.waffle.looks
Item {
id: root
anchors.centerIn: parent
required property Item realContentItem
property real verticalPadding: 8
property real horizontalPadding: 10
implicitWidth: realContent.implicitWidth + 2 * 2
implicitHeight: realContent.implicitHeight + 2 * 2
WAmbientShadow {
target: realContent
}
Rectangle {
id: realContent
z: 1
anchors.centerIn: parent
implicitWidth: root.realContentItem.implicitWidth + root.horizontalPadding * 2
implicitHeight: root.realContentItem.implicitHeight + root.verticalPadding * 2
color: Looks.colors.bg1
radius: Looks.radius.medium
children: [root.realContentItem]
}
}
+38 -2
View File
@@ -11,15 +11,43 @@ import Quickshell.Services.Pipewire
Singleton {
id: root
// Misc props
property bool ready: Pipewire.defaultAudioSink?.ready ?? false
property PwNode sink: Pipewire.defaultAudioSink
property PwNode source: Pipewire.defaultAudioSource
readonly property real hardMaxValue: 2.00 // People keep joking about setting volume to 5172% so...
property string audioTheme: Config.options.sounds.theme
property real value: sink?.audio.volume ?? 0
function friendlyDeviceName(node) {
return (node.nickname || node.description || Translation.tr("Unknown"));
}
function appNodeDisplayName(node) {
return (node.properties["application.name"] || node.description || node.name)
}
// Lists
function correctType(node, isSink) {
return (node.isSink === isSink) && node.audio
}
function appNodes(isSink) {
return Pipewire.nodes.values.filter((node) => { // Should be list<PwNode> but it breaks ScriptModel
return root.correctType(node, isSink) && node.isStream
})
}
function devices(isSink) {
return Pipewire.nodes.values.filter(node => {
return root.correctType(node, isSink) && !node.isStream
})
}
readonly property list<var> outputAppNodes: root.appNodes(true)
readonly property list<var> inputAppNodes: root.appNodes(false)
readonly property list<var> outputDevices: root.devices(true)
readonly property list<var> inputDevices: root.devices(false)
// Signals
signal sinkProtectionTriggered(string reason);
// Controls
function toggleMute() {
Audio.sink.audio.muted = !Audio.sink.audio.muted
}
@@ -39,8 +67,16 @@ Singleton {
const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
Audio.sink.audio.volume -= step;
}
function setDefaultSink(node) {
Pipewire.preferredDefaultAudioSink = node;
}
function setDefaultSource(node) {
Pipewire.preferredDefaultAudioSource = node;
}
// Internals
PwObjectTracker {
objects: [sink, source]
}