added starting funtionality
This commit is contained in:
@@ -2,7 +2,7 @@ pragma Singleton
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
readonly property string fontFamily: "Noto Sans"
|
readonly property string fontFamily: "CaskaydiaCove Nerd Font Mono"
|
||||||
readonly property string iconFontFamily: "Material Symbols Outlined"
|
readonly property string iconFontFamily: "Material Symbols Outlined"
|
||||||
// Material Symbols is a variable font. Left unset, glyphs fall back to the
|
// Material Symbols is a variable font. Left unset, glyphs fall back to the
|
||||||
// font's default optical size (24px), which is tuned for larger rendering
|
// font's default optical size (24px), which is tuned for larger rendering
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ QtObject {
|
|||||||
// identifier as a signal handler slot, not a regular property.
|
// identifier as a signal handler slot, not a regular property.
|
||||||
readonly property color primaryText: background
|
readonly property color primaryText: background
|
||||||
readonly property color urgent: paletteColor(1)
|
readonly property color urgent: paletteColor(1)
|
||||||
readonly property color muted: paletteColor(8)
|
// paletteColor(8) alone renders quite dark/low-contrast for text (it's
|
||||||
|
// meant as a muted *background* tone in most wal palettes) — lighten it
|
||||||
|
// so muted labels stay legible against the bar's dark background.
|
||||||
|
readonly property color muted: Qt.lighter(paletteColor(8), 2.2)
|
||||||
readonly property color surface: Qt.lighter(background, 1.35)
|
readonly property color surface: Qt.lighter(background, 1.35)
|
||||||
|
|
||||||
// QtObject has no default property, so the FileView is attached via a
|
// QtObject has no default property, so the FileView is attached via a
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
spacing: Appearance.spacing
|
spacing: Appearance.spacing
|
||||||
Tray {}
|
Tray {}
|
||||||
NetworkIndicator {}
|
NetworkIndicator { barWindow: bar }
|
||||||
BluetoothIndicator {}
|
BluetoothIndicator {}
|
||||||
Volume {}
|
Volume { barWindow: bar }
|
||||||
PowerButton {}
|
PowerButton { barWindow: bar }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import QtQuick
|
|||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import Quickshell.Hyprland
|
||||||
import "../../config"
|
import "../../config"
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -9,6 +10,12 @@ Item {
|
|||||||
implicitWidth: label.implicitWidth + 4
|
implicitWidth: label.implicitWidth + 4
|
||||||
implicitHeight: Appearance.barHeight
|
implicitHeight: Appearance.barHeight
|
||||||
|
|
||||||
|
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
|
||||||
|
// anchor to the bar itself rather than to this icon, keeping it a fixed
|
||||||
|
// distance from the bar and the screen edge regardless of where this
|
||||||
|
// icon sits in the row.
|
||||||
|
required property var barWindow
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: label
|
id: label
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -22,7 +29,10 @@ Item {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: menu.visible = !menu.visible
|
onClicked: {
|
||||||
|
menu.visible = !menu.visible
|
||||||
|
grab.active = menu.visible
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process { id: proc }
|
Process { id: proc }
|
||||||
@@ -31,79 +41,134 @@ Item {
|
|||||||
id: menu
|
id: menu
|
||||||
visible: false
|
visible: false
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
implicitWidth: 170
|
implicitWidth: 210
|
||||||
implicitHeight: list.model.length * 32 + (list.model.length - 1) * 2 + 16
|
// Same padding as Volume.qml's panel.
|
||||||
|
readonly property int contentPadding: 16
|
||||||
|
readonly property int labelOffset: -135
|
||||||
|
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
|
||||||
|
|
||||||
anchor {
|
anchor {
|
||||||
item: root
|
window: root.barWindow
|
||||||
|
// Unlike anchor.item, anchor.window doesn't default rect to the
|
||||||
|
// window's own bounds — set it explicitly so edges/gravity below
|
||||||
|
// resolve against the full bar (and thus the true screen edges)
|
||||||
|
// instead of a near-zero rect at the window's origin.
|
||||||
|
rect.x: 0
|
||||||
|
rect.y: 0
|
||||||
|
rect.width: root.barWindow.width
|
||||||
|
rect.height: root.barWindow.height
|
||||||
edges: Edges.Bottom | Edges.Right
|
edges: Edges.Bottom | Edges.Right
|
||||||
gravity: Edges.Bottom | Edges.Left
|
gravity: Edges.Bottom | Edges.Left
|
||||||
margins.top: 4
|
margins.bottom: -4
|
||||||
|
margins.right: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
id: grab
|
||||||
|
windows: [menu]
|
||||||
|
onCleared: menu.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: Appearance.radius
|
radius: 0
|
||||||
color: Colors.surface
|
color: Colors.surface
|
||||||
border.color: Colors.muted
|
border.color: Colors.muted
|
||||||
border.width: 1
|
border.width: 3
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
id: mainColumn
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 8
|
anchors.margins: menu.contentPadding
|
||||||
spacing: 2
|
spacing: 10
|
||||||
|
|
||||||
Repeater {
|
// Header — same title layout as Volume.qml's panel: the bar
|
||||||
id: list
|
// icon large, plus a bold title.
|
||||||
model: [
|
RowLayout {
|
||||||
// "iconSize" is optional — omit it to use the 15px default below.
|
id: header
|
||||||
// Material Symbols glyphs aren't all the same visual weight at a
|
width: parent.width
|
||||||
// given size (a padlock silhouette reads bulkier than a thin arrow),
|
spacing: 8
|
||||||
// so nudge individual icons here to balance them by eye.
|
|
||||||
{ label: "Lock", icon: "lock", iconSize: 13, cmd: ["hyprlock"] },
|
|
||||||
{ label: "Log out", icon: "logout", cmd: ["hyprctl", "dispatch", "exit"] },
|
|
||||||
{ label: "Reboot", icon: "restart_alt", cmd: ["systemctl", "reboot"] },
|
|
||||||
{ label: "Shut down", icon: "power_settings_new", cmd: ["systemctl", "poweroff"] }
|
|
||||||
]
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
Text {
|
||||||
id: entry
|
text: label.text
|
||||||
required property var modelData
|
font.family: Appearance.iconFontFamily
|
||||||
width: parent ? parent.width : 0
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
height: 32
|
font.pixelSize: 30
|
||||||
radius: Appearance.radiusSmall
|
color: Colors.foreground
|
||||||
color: entryMouse.containsMouse ? Colors.primary : "transparent"
|
}
|
||||||
|
|
||||||
RowLayout {
|
Text {
|
||||||
anchors.fill: parent
|
Layout.fillWidth: true
|
||||||
anchors.leftMargin: 8
|
text: "Power"
|
||||||
spacing: 8
|
font.family: Appearance.fontFamily
|
||||||
Text {
|
font.pixelSize: 18
|
||||||
Layout.preferredWidth: 20
|
font.bold: true
|
||||||
horizontalAlignment: Text.AlignHCenter
|
color: Colors.foreground
|
||||||
text: entry.modelData.icon
|
}
|
||||||
font.family: Appearance.iconFontFamily
|
}
|
||||||
font.variableAxes: Appearance.iconFontAxes
|
|
||||||
font.pixelSize: entry.modelData.iconSize || 15
|
Column {
|
||||||
color: entryMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
width: parent.width
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: list
|
||||||
|
model: [
|
||||||
|
{ label: "Lock", icon: "lock", cmd: ["hyprlock"] },
|
||||||
|
{ label: "Suspend", icon: "bedtime", cmd: ["systemctl", "suspend"] },
|
||||||
|
{ label: "Hibernate", icon: "ac_unit", cmd: ["systemctl", "hibernate"] },
|
||||||
|
{ label: "Log out", icon: "logout", cmd: ["hyprctl", "dispatch", "exit"] },
|
||||||
|
{ label: "Reboot", icon: "restart_alt", cmd: ["systemctl", "reboot"] },
|
||||||
|
{ label: "Shut down", icon: "power_settings_new", cmd: ["systemctl", "poweroff"] }
|
||||||
|
]
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: entry
|
||||||
|
required property var modelData
|
||||||
|
width: parent ? parent.width : 0
|
||||||
|
height: 40
|
||||||
|
radius: Appearance.radiusSmall
|
||||||
|
color: (!entry.modelData.disabled && entryMouse.containsMouse) ? Colors.primary : "transparent"
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
spacing: 8
|
||||||
|
Text {
|
||||||
|
Layout.preferredWidth: 25
|
||||||
|
Layout.minimumWidth: 25
|
||||||
|
Layout.maximumWidth: 25
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
text: entry.modelData.icon
|
||||||
|
font.family: Appearance.iconFontFamily
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: entry.modelData.iconSize || 15
|
||||||
|
opacity: entry.modelData.disabled ? 0.4 : 1
|
||||||
|
color: entryMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
Layout.leftMargin: menu.labelOffset + (entry.modelData.textOffset || 0)
|
||||||
|
text: entry.modelData.label
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: 16
|
||||||
|
opacity: entry.modelData.disabled ? 0.4 : 1
|
||||||
|
color: entryMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Text {
|
|
||||||
text: entry.modelData.label
|
|
||||||
font.family: Appearance.fontFamily
|
|
||||||
font.pixelSize: Appearance.fontSize
|
|
||||||
color: entryMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: entryMouse
|
id: entryMouse
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
enabled: !entry.modelData.disabled
|
||||||
cursorShape: Qt.PointingHandCursor
|
hoverEnabled: true
|
||||||
onClicked: {
|
cursorShape: Qt.PointingHandCursor
|
||||||
proc.command = entry.modelData.cmd
|
onClicked: {
|
||||||
proc.running = true
|
proc.command = entry.modelData.cmd
|
||||||
menu.visible = false
|
proc.running = true
|
||||||
|
menu.visible = false
|
||||||
|
grab.active = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
import Quickshell.Services.Pipewire
|
import Quickshell.Services.Pipewire
|
||||||
import "../../config"
|
import "../../config"
|
||||||
|
|
||||||
@@ -7,11 +10,87 @@ Item {
|
|||||||
implicitWidth: label.implicitWidth + 4
|
implicitWidth: label.implicitWidth + 4
|
||||||
implicitHeight: Appearance.barHeight
|
implicitHeight: Appearance.barHeight
|
||||||
|
|
||||||
readonly property var sink: Pipewire.defaultAudioSink
|
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
|
||||||
readonly property real vol: (sink && sink.audio) ? sink.audio.volume : 0
|
// anchor to the bar itself rather than to this icon, keeping it a fixed
|
||||||
readonly property bool muted: (sink && sink.audio) ? sink.audio.muted : false
|
// distance from the bar and the screen edge regardless of where this
|
||||||
|
// icon sits in the row.
|
||||||
|
required property var barWindow
|
||||||
|
|
||||||
PwObjectTracker { objects: root.sink ? [root.sink] : [] }
|
readonly property var sink: Pipewire.defaultAudioSink
|
||||||
|
readonly property var source: Pipewire.defaultAudioSource
|
||||||
|
readonly property var nodes: Pipewire.nodes ? Pipewire.nodes.values : []
|
||||||
|
|
||||||
|
readonly property real outputVolume: (root.sink && root.sink.audio) ? root.sink.audio.volume : 0
|
||||||
|
readonly property bool outputMuted: (root.sink && root.sink.audio) ? root.sink.audio.muted : false
|
||||||
|
readonly property real inputVolume: (root.source && root.source.audio) ? root.source.audio.volume : 0
|
||||||
|
readonly property bool inputMuted: (root.source && root.source.audio) ? root.source.audio.muted : false
|
||||||
|
|
||||||
|
readonly property var candidateSinks: root.nodes.filter(function(n) { return n && n.isSink && !n.isStream })
|
||||||
|
readonly property var candidateSources: root.nodes.filter(function(n) { return n && !n.isSink && !n.isStream && n.audio })
|
||||||
|
|
||||||
|
PwObjectTracker { objects: root.candidateSinks }
|
||||||
|
PwObjectTracker { objects: root.candidateSources }
|
||||||
|
|
||||||
|
// Live mic level for the input gauge below — separate from inputVolume
|
||||||
|
// (the configured gain), this tracks the actual incoming signal.
|
||||||
|
// Only runs while the panel is open, to skip metering when unused.
|
||||||
|
PwNodePeakMonitor {
|
||||||
|
id: inputPeak
|
||||||
|
node: root.source
|
||||||
|
enabled: panel.visible
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Device label/icon heuristics, ported from omarchy's audio panel
|
||||||
|
// Model.js (github.com/basecamp/omarchy shell/plugins/panels/audio/Model.js),
|
||||||
|
// adapted to this project's Material Symbols icon set instead of Nerd Font.
|
||||||
|
function friendlyDeviceLabel(text) {
|
||||||
|
var out = String(text || "").trim()
|
||||||
|
out = out.replace(/^sof-soundwire\s+/i, "")
|
||||||
|
out = out.replace(/^built-?in audio\s+/i, "")
|
||||||
|
out = out.replace(/\s+Output$/i, "")
|
||||||
|
out = out.replace(/\s+Input$/i, "")
|
||||||
|
out = out.replace(/Microphones/gi, "Microphone")
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function deviceLabel(node) {
|
||||||
|
if (!node) return "Unknown"
|
||||||
|
var nick = friendlyDeviceLabel(node.nickname || "")
|
||||||
|
if (nick) return nick
|
||||||
|
return friendlyDeviceLabel(node.description || node.name || "Unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
function deviceBlob(node) {
|
||||||
|
if (!node) return ""
|
||||||
|
var p = node.properties || {}
|
||||||
|
return [node.name, node.description, node.nickname,
|
||||||
|
p["device.icon-name"] || "", p["device.product.name"] || ""].join(" ").toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHeadphoneNode(node) {
|
||||||
|
var blob = deviceBlob(node)
|
||||||
|
return blob.indexOf("headphone") !== -1 || blob.indexOf("headset") !== -1
|
||||||
|
|| blob.indexOf("earbud") !== -1 || blob.indexOf("earphone") !== -1
|
||||||
|
|| blob.indexOf("airpod") !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function sinkIcon(node) {
|
||||||
|
if (!node) return "speaker"
|
||||||
|
if (isHeadphoneNode(node)) return "headphones"
|
||||||
|
var blob = deviceBlob(node)
|
||||||
|
if (blob.indexOf("bluetooth") !== -1) return "bluetooth_audio"
|
||||||
|
if (blob.indexOf("hdmi") !== -1 || blob.indexOf("display") !== -1) return "tv"
|
||||||
|
return "speaker"
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceIcon(node) {
|
||||||
|
if (!node) return "mic"
|
||||||
|
var blob = deviceBlob(node)
|
||||||
|
if (blob.indexOf("headset") !== -1) return "headphones"
|
||||||
|
if (blob.indexOf("bluetooth") !== -1) return "bluetooth_audio"
|
||||||
|
if (blob.indexOf("webcam") !== -1 || blob.indexOf("camera") !== -1) return "videocam"
|
||||||
|
return "mic"
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: label
|
id: label
|
||||||
@@ -20,19 +99,17 @@ Item {
|
|||||||
font.variableAxes: Appearance.iconFontAxes
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
font.pixelSize: 16
|
font.pixelSize: 16
|
||||||
color: Colors.foreground
|
color: Colors.foreground
|
||||||
text: {
|
text: (root.outputMuted || root.outputVolume <= 0) ? "volume_off"
|
||||||
if (root.muted || root.vol <= 0) return "volume_off"
|
: (root.outputVolume < 0.5 ? "volume_down" : "volume_up")
|
||||||
if (root.vol < 0.5) return "volume_down"
|
|
||||||
return "volume_up"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
acceptedButtons: Qt.LeftButton
|
onClicked: {
|
||||||
onClicked: if (root.sink && root.sink.audio) root.sink.audio.muted = !root.sink.audio.muted
|
panel.visible = !panel.visible
|
||||||
|
grab.active = panel.visible
|
||||||
|
}
|
||||||
onWheel: (wheel) => {
|
onWheel: (wheel) => {
|
||||||
if (!root.sink || !root.sink.audio) return
|
if (!root.sink || !root.sink.audio) return
|
||||||
const step = 0.05
|
const step = 0.05
|
||||||
@@ -40,4 +117,330 @@ Item {
|
|||||||
root.sink.audio.volume = Math.max(0, Math.min(1, root.sink.audio.volume + delta))
|
root.sink.audio.volume = Math.max(0, Math.min(1, root.sink.audio.volume + delta))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Reusable pieces -----------------------------------------------
|
||||||
|
|
||||||
|
component VolumeSlider: Item {
|
||||||
|
id: sliderRoot
|
||||||
|
property real value: 0
|
||||||
|
property bool enabled: true
|
||||||
|
signal moved(real newValue)
|
||||||
|
implicitHeight: 16
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: track
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width
|
||||||
|
height: 4
|
||||||
|
radius: 0
|
||||||
|
color: Colors.muted
|
||||||
|
opacity: 0.35
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
width: track.width * Math.max(0, Math.min(1, sliderRoot.value))
|
||||||
|
height: 4
|
||||||
|
radius: 0
|
||||||
|
color: Colors.primary
|
||||||
|
opacity: sliderRoot.enabled ? 1 : 0.4
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
width: 12
|
||||||
|
height: 12
|
||||||
|
radius: 6
|
||||||
|
color: Colors.primary
|
||||||
|
opacity: sliderRoot.enabled ? 1 : 0.4
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
x: Math.max(0, Math.min(track.width - width, track.width * sliderRoot.value - width / 2))
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: sliderRoot.enabled
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
function updateFromX(mx) {
|
||||||
|
sliderRoot.moved(Math.max(0, Math.min(1, mx / track.width)))
|
||||||
|
}
|
||||||
|
onPressed: (mouse) => updateFromX(mouse.x)
|
||||||
|
onPositionChanged: (mouse) => { if (pressed) updateFromX(mouse.x) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read-only live level gauge — same track styling as VolumeSlider but no
|
||||||
|
// handle/interaction, filled from an actual signal level instead of a
|
||||||
|
// configured value.
|
||||||
|
component LevelMeter: Item {
|
||||||
|
id: meterRoot
|
||||||
|
property real level: 0
|
||||||
|
implicitHeight: 4
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width
|
||||||
|
height: 4
|
||||||
|
radius: 0
|
||||||
|
color: Colors.muted
|
||||||
|
opacity: 0.25
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
height: 4
|
||||||
|
radius: 0
|
||||||
|
color: Colors.paletteColor(2)
|
||||||
|
width: parent.width * Math.max(0, Math.min(1, meterRoot.level))
|
||||||
|
Behavior on width { NumberAnimation { duration: 80 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component ToggleSwitch: Item {
|
||||||
|
id: toggleRoot
|
||||||
|
property bool checked: false
|
||||||
|
signal toggled()
|
||||||
|
implicitWidth: 34
|
||||||
|
implicitHeight: 18
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 0
|
||||||
|
color: toggleRoot.checked ? Colors.primary : Colors.muted
|
||||||
|
opacity: toggleRoot.checked ? 1 : 0.35
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
width: parent.height - 4
|
||||||
|
height: parent.height - 4
|
||||||
|
radius: 0
|
||||||
|
color: Colors.background
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
x: toggleRoot.checked ? parent.width - width - 2 : 2
|
||||||
|
Behavior on x { NumberAnimation { duration: Appearance.animationDuration } }
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: toggleRoot.toggled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component DeviceRow: Rectangle {
|
||||||
|
id: deviceRow
|
||||||
|
required property var node
|
||||||
|
required property string icon
|
||||||
|
required property bool active
|
||||||
|
signal activate()
|
||||||
|
width: parent ? parent.width : 0
|
||||||
|
height: 30
|
||||||
|
radius: 0
|
||||||
|
color: rowMouse.containsMouse ? Colors.primary
|
||||||
|
: (deviceRow.active ? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18) : "transparent")
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
spacing: 8
|
||||||
|
Text {
|
||||||
|
Layout.preferredWidth: 18
|
||||||
|
Layout.minimumWidth: 18
|
||||||
|
Layout.maximumWidth: 18
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: deviceRow.icon
|
||||||
|
font.family: Appearance.iconFontFamily
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.deviceLabel(deviceRow.node)
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSize
|
||||||
|
font.bold: deviceRow.active
|
||||||
|
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
id: rowMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: deviceRow.activate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Panel -----------------------------------------------------------
|
||||||
|
|
||||||
|
PopupWindow {
|
||||||
|
id: panel
|
||||||
|
visible: false
|
||||||
|
color: "transparent"
|
||||||
|
// Padding between the panel's border and its content. implicitWidth
|
||||||
|
// is contentWidth + padding on both sides, so the content area
|
||||||
|
// itself stays a fixed size regardless of how much padding surrounds it.
|
||||||
|
readonly property int contentWidth: 244
|
||||||
|
readonly property int contentPadding: 16
|
||||||
|
implicitWidth: contentWidth + contentPadding * 2
|
||||||
|
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
|
||||||
|
|
||||||
|
anchor {
|
||||||
|
window: root.barWindow
|
||||||
|
// Unlike anchor.item, anchor.window doesn't default rect to the
|
||||||
|
// window's own bounds — set it explicitly so edges/gravity below
|
||||||
|
// resolve against the full bar (and thus the true screen edges)
|
||||||
|
// instead of a near-zero rect at the window's origin.
|
||||||
|
rect.x: 0
|
||||||
|
rect.y: 0
|
||||||
|
rect.width: root.barWindow.width
|
||||||
|
rect.height: root.barWindow.height
|
||||||
|
edges: Edges.Bottom | Edges.Right
|
||||||
|
gravity: Edges.Bottom | Edges.Left
|
||||||
|
margins.bottom: -4
|
||||||
|
margins.right: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
id: grab
|
||||||
|
windows: [panel]
|
||||||
|
onCleared: panel.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 0
|
||||||
|
color: Colors.surface
|
||||||
|
border.color: Colors.muted
|
||||||
|
border.width: 3
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: panel.contentPadding
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
// Header
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: label.text
|
||||||
|
font.family: Appearance.iconFontFamily
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 40
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: "Audio"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.bold: true
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleSwitch {
|
||||||
|
checked: !root.outputMuted
|
||||||
|
onToggled: if (root.sink && root.sink.audio) root.sink.audio.muted = !root.sink.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: "OUTPUT"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: Math.round(root.outputVolume * 100) + "%"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VolumeSlider {
|
||||||
|
width: parent.width
|
||||||
|
value: root.outputVolume
|
||||||
|
enabled: !!root.sink
|
||||||
|
onMoved: (v) => { if (root.sink && root.sink.audio) root.sink.audio.volume = v }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 2
|
||||||
|
Repeater {
|
||||||
|
model: root.candidateSinks
|
||||||
|
DeviceRow {
|
||||||
|
required property var modelData
|
||||||
|
node: modelData
|
||||||
|
icon: root.sinkIcon(modelData)
|
||||||
|
active: !!(root.sink && modelData && root.sink.id === modelData.id)
|
||||||
|
onActivate: Pipewire.preferredDefaultAudioSink = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: "INPUT"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: Math.round(root.inputVolume * 100) + "%"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VolumeSlider {
|
||||||
|
width: parent.width
|
||||||
|
value: root.inputVolume
|
||||||
|
enabled: !!root.source
|
||||||
|
onMoved: (v) => { if (root.source && root.source.audio) root.source.audio.volume = v }
|
||||||
|
}
|
||||||
|
|
||||||
|
LevelMeter {
|
||||||
|
width: parent.width
|
||||||
|
level: inputPeak.peak
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 2
|
||||||
|
Repeater {
|
||||||
|
model: root.candidateSources
|
||||||
|
DeviceRow {
|
||||||
|
required property var modelData
|
||||||
|
node: modelData
|
||||||
|
icon: root.sourceIcon(modelData)
|
||||||
|
active: !!(root.source && modelData && root.source.id === modelData.id)
|
||||||
|
onActivate: Pipewire.preferredDefaultAudioSource = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user