mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-08-05 13:57:36 +00:00
628 lines
19 KiB
QML
628 lines
19 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import QtQuick.Effects
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import Quickshell.Services.Pipewire
|
|
import qs.Commons
|
|
import qs.Widgets
|
|
import qs.Services.Hardware
|
|
import qs.Services.Media
|
|
import qs.Services.System
|
|
|
|
// Unified OSD component that displays volume, input volume, and brightness changes
|
|
Variants {
|
|
model: Quickshell.screens.filter(screen =>
|
|
(Settings.data.osd.monitors.includes(screen.name) ||
|
|
Settings.data.osd.monitors.length === 0) &&
|
|
Settings.data.osd.enabled
|
|
)
|
|
|
|
delegate: Loader {
|
|
id: root
|
|
|
|
required property ShellScreen modelData
|
|
|
|
active: false
|
|
|
|
// OSD State
|
|
property string currentOSDType: "" // "volume", "inputVolume", "brightness", or ""
|
|
|
|
// Audio Output State
|
|
property real lastKnownVolume: -1
|
|
property bool volumeInitialized: false
|
|
property bool muteInitialized: false
|
|
|
|
// Audio Input State
|
|
property real lastKnownInputVolume: -1
|
|
property bool inputInitialized: false
|
|
|
|
// Brightness State
|
|
property real lastUpdatedBrightness: 0
|
|
property bool brightnessInitialized: false
|
|
|
|
// Current values (computed properties)
|
|
readonly property real currentVolume: AudioService.volume
|
|
readonly property bool isMuted: AudioService.muted
|
|
readonly property real currentInputVolume: AudioService.inputVolume
|
|
readonly property bool isInputMuted: AudioService.inputMuted
|
|
readonly property real currentBrightness: lastUpdatedBrightness
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
function getIcon() {
|
|
switch (currentOSDType) {
|
|
case "volume":
|
|
if (isMuted) return "volume-mute"
|
|
if (currentVolume <= Number.EPSILON) return "volume-zero"
|
|
return currentVolume <= 0.5 ? "volume-low" : "volume-high"
|
|
|
|
case "inputVolume":
|
|
return isInputMuted ? "microphone-off" : "microphone"
|
|
|
|
case "brightness":
|
|
return currentBrightness <= 0.5 ? "brightness-low" : "brightness-high"
|
|
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
function getCurrentValue() {
|
|
switch (currentOSDType) {
|
|
case "volume": return isMuted ? 0 : currentVolume
|
|
case "inputVolume": return isInputMuted ? 0 : currentInputVolume
|
|
case "brightness": return currentBrightness
|
|
default: return 0
|
|
}
|
|
}
|
|
|
|
function getMaxValue() {
|
|
if (currentOSDType === "volume" || currentOSDType === "inputVolume") {
|
|
return Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
|
}
|
|
return 1.0
|
|
}
|
|
|
|
function getDisplayPercentage() {
|
|
const value = getCurrentValue()
|
|
const max = getMaxValue()
|
|
const pct = Math.round(Math.min(max, value) * 100)
|
|
return pct + "%"
|
|
}
|
|
|
|
function getProgressColor() {
|
|
const isMutedState = (currentOSDType === "volume" && isMuted) ||
|
|
(currentOSDType === "inputVolume" && isInputMuted)
|
|
return isMutedState ? Color.mError : Color.mPrimary
|
|
}
|
|
|
|
function getIconColor() {
|
|
const isMutedState = (currentOSDType === "volume" && isMuted) ||
|
|
(currentOSDType === "inputVolume" && isInputMuted)
|
|
return isMutedState ? Color.mError : Color.mOnSurface
|
|
}
|
|
|
|
// ============================================================================
|
|
// Audio Initialization
|
|
// ============================================================================
|
|
|
|
function initializeAudioValues() {
|
|
// Initialize output volume
|
|
if (AudioService.sink?.ready && AudioService.sink?.audio && lastKnownVolume < 0) {
|
|
const vol = AudioService.volume
|
|
if (vol !== undefined && !isNaN(vol)) {
|
|
lastKnownVolume = vol
|
|
volumeInitialized = true
|
|
muteInitialized = true
|
|
}
|
|
}
|
|
|
|
// Initialize input volume
|
|
if (AudioService.hasInput && AudioService.source?.ready &&
|
|
AudioService.source?.audio && lastKnownInputVolume < 0) {
|
|
const inputVol = AudioService.inputVolume
|
|
if (inputVol !== undefined && !isNaN(inputVol)) {
|
|
lastKnownInputVolume = inputVol
|
|
inputInitialized = true
|
|
}
|
|
}
|
|
}
|
|
|
|
function resetOutputInit() {
|
|
lastKnownVolume = -1
|
|
volumeInitialized = false
|
|
muteInitialized = false
|
|
Qt.callLater(initializeAudioValues)
|
|
}
|
|
|
|
function resetInputInit() {
|
|
lastKnownInputVolume = -1
|
|
inputInitialized = false
|
|
Qt.callLater(initializeAudioValues)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Brightness Handling
|
|
// ============================================================================
|
|
|
|
function connectBrightnessMonitors() {
|
|
for (let i = 0; i < BrightnessService.monitors.length; i++) {
|
|
const monitor = BrightnessService.monitors[i]
|
|
monitor.brightnessUpdated.disconnect(onBrightnessChanged)
|
|
monitor.brightnessUpdated.connect(onBrightnessChanged)
|
|
}
|
|
}
|
|
|
|
function onBrightnessChanged(newBrightness) {
|
|
lastUpdatedBrightness = newBrightness
|
|
|
|
if (!brightnessInitialized) {
|
|
brightnessInitialized = true
|
|
return
|
|
}
|
|
|
|
showOSD("brightness")
|
|
}
|
|
|
|
// ============================================================================
|
|
// OSD Display Control
|
|
// ============================================================================
|
|
|
|
function showOSD(type) {
|
|
currentOSDType = type
|
|
|
|
if (!root.active) {
|
|
root.active = true
|
|
}
|
|
|
|
if (root.item) {
|
|
root.item.showOSD()
|
|
} else {
|
|
Qt.callLater(() => {
|
|
if (root.item) root.item.showOSD()
|
|
})
|
|
}
|
|
}
|
|
|
|
function hideOSD() {
|
|
if (root.item?.osdItem) {
|
|
root.item.osdItem.hideImmediately()
|
|
} else if (root.active) {
|
|
root.active = false
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Signal Connections
|
|
// ============================================================================
|
|
|
|
// Pipewire state monitoring
|
|
Connections {
|
|
target: Pipewire
|
|
|
|
function onReadyChanged() {
|
|
if (Pipewire.ready) Qt.callLater(initializeAudioValues)
|
|
}
|
|
|
|
function onDefaultAudioSinkChanged() {
|
|
resetOutputInit()
|
|
}
|
|
|
|
function onDefaultAudioSourceChanged() {
|
|
resetInputInit()
|
|
}
|
|
}
|
|
|
|
// AudioService monitoring
|
|
Connections {
|
|
target: AudioService
|
|
|
|
function onSinkChanged() {
|
|
if (AudioService.sink?.ready && AudioService.sink?.audio) {
|
|
resetOutputInit()
|
|
}
|
|
}
|
|
|
|
function onSourceChanged() {
|
|
if (AudioService.hasInput && AudioService.source?.ready && AudioService.source?.audio) {
|
|
resetInputInit()
|
|
}
|
|
}
|
|
|
|
function onVolumeChanged() {
|
|
if (lastKnownVolume < 0) {
|
|
initializeAudioValues()
|
|
if (lastKnownVolume < 0) return
|
|
}
|
|
if (!volumeInitialized) return
|
|
|
|
if (Math.abs(AudioService.volume - lastKnownVolume) > 0.001) {
|
|
lastKnownVolume = AudioService.volume
|
|
showOSD("volume")
|
|
}
|
|
}
|
|
|
|
function onMutedChanged() {
|
|
if (lastKnownVolume < 0) {
|
|
initializeAudioValues()
|
|
if (lastKnownVolume < 0) return
|
|
}
|
|
if (!muteInitialized) return
|
|
showOSD("volume")
|
|
}
|
|
|
|
function onInputVolumeChanged() {
|
|
if (!AudioService.hasInput) return
|
|
|
|
if (lastKnownInputVolume < 0) {
|
|
initializeAudioValues()
|
|
if (lastKnownInputVolume < 0) return
|
|
}
|
|
if (!inputInitialized) return
|
|
|
|
if (Math.abs(AudioService.inputVolume - lastKnownInputVolume) > 0.001) {
|
|
lastKnownInputVolume = AudioService.inputVolume
|
|
showOSD("inputVolume")
|
|
}
|
|
}
|
|
|
|
function onInputMutedChanged() {
|
|
if (!AudioService.hasInput) return
|
|
|
|
if (lastKnownInputVolume < 0) {
|
|
initializeAudioValues()
|
|
if (lastKnownInputVolume < 0) return
|
|
}
|
|
if (!inputInitialized) return
|
|
showOSD("inputVolume")
|
|
}
|
|
}
|
|
|
|
// Brightness monitoring
|
|
Connections {
|
|
target: BrightnessService
|
|
function onMonitorsChanged() {
|
|
connectBrightnessMonitors()
|
|
}
|
|
}
|
|
|
|
// Initialization timers
|
|
Timer {
|
|
id: initTimer
|
|
interval: 500
|
|
running: true
|
|
onTriggered: {
|
|
if (Pipewire.ready) initializeAudioValues()
|
|
muteInitialized = true
|
|
connectBrightnessMonitors()
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: reinitTimer
|
|
interval: 1000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: {
|
|
if (!Pipewire.ready) return
|
|
|
|
const needsOutputInit = lastKnownVolume < 0
|
|
const needsInputInit = AudioService.hasInput && lastKnownInputVolume < 0
|
|
|
|
if (needsOutputInit || needsInputInit) {
|
|
initializeAudioValues()
|
|
|
|
// Stop timer if both are initialized
|
|
const outputDone = lastKnownVolume >= 0
|
|
const inputDone = !AudioService.hasInput || lastKnownInputVolume >= 0
|
|
if (outputDone && inputDone) {
|
|
running = false
|
|
}
|
|
} else {
|
|
running = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Visual Component
|
|
// ============================================================================
|
|
|
|
sourceComponent: PanelWindow {
|
|
id: panel
|
|
screen: modelData
|
|
|
|
// Position configuration
|
|
readonly property string location: Settings.data.osd?.location || "top_right"
|
|
readonly property bool isTop: location === "top" || location.startsWith("top")
|
|
readonly property bool isBottom: location === "bottom" || location.startsWith("bottom")
|
|
readonly property bool isLeft: location.includes("_left") || location === "left"
|
|
readonly property bool isRight: location.includes("_right") || location === "right"
|
|
readonly property bool verticalMode: location === "left" || location === "right"
|
|
|
|
// Dimensions
|
|
readonly property int hWidth: Math.round(320 * Style.uiScaleRatio)
|
|
readonly property int hHeight: Math.round(72 * Style.uiScaleRatio)
|
|
readonly property int vWidth: Math.round(72 * Style.uiScaleRatio)
|
|
readonly property int vHeight: Math.round(280 * Style.uiScaleRatio)
|
|
readonly property int barThickness: {
|
|
const base = Math.max(8, Math.round(8 * Style.uiScaleRatio))
|
|
return base % 2 === 0 ? base : base + 1
|
|
}
|
|
|
|
anchors.top: isTop
|
|
anchors.bottom: isBottom
|
|
anchors.left: isLeft
|
|
anchors.right: isRight
|
|
|
|
function calculateMargin(isAnchored, position) {
|
|
if (!isAnchored) return 0
|
|
|
|
let base = Style.marginM
|
|
if (Settings.data.bar.position === position) {
|
|
const isVertical = position === "top" || position === "bottom"
|
|
const floatExtra = Settings.data.bar.floating ?
|
|
(isVertical ? Settings.data.bar.marginVertical : Settings.data.bar.marginHorizontal) * Style.marginXL : 0
|
|
return Style.barHeight + base + floatExtra
|
|
}
|
|
return base
|
|
}
|
|
|
|
margins.top: calculateMargin(anchors.top, "top")
|
|
margins.bottom: calculateMargin(anchors.bottom, "bottom")
|
|
margins.left: calculateMargin(anchors.left, "left")
|
|
margins.right: calculateMargin(anchors.right, "right")
|
|
|
|
implicitWidth: verticalMode ? vWidth : hWidth
|
|
implicitHeight: verticalMode ? vHeight : hHeight
|
|
color: Color.transparent
|
|
|
|
WlrLayershell.namespace: "noctalia-osd-" + (screen?.name || "unknown")
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
WlrLayershell.layer: Settings.data.osd?.overlayLayer ? WlrLayer.Overlay : WlrLayer.Top
|
|
exclusionMode: PanelWindow.ExclusionMode.Ignore
|
|
|
|
Item {
|
|
id: osdItem
|
|
anchors.fill: parent
|
|
visible: false
|
|
opacity: 0
|
|
scale: 0.85
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Style.animationNormal
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
|
|
Behavior on scale {
|
|
NumberAnimation {
|
|
duration: Style.animationNormal
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: hideTimer
|
|
interval: Settings.data.osd.autoHideMs
|
|
onTriggered: osdItem.hide()
|
|
}
|
|
|
|
Timer {
|
|
id: visibilityTimer
|
|
interval: Style.animationNormal + 50
|
|
onTriggered: {
|
|
osdItem.visible = false
|
|
root.currentOSDType = ""
|
|
root.active = false
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: background
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginM * 1.5
|
|
radius: Style.radiusL
|
|
color: Qt.alpha(Color.mSurface, Settings.data.osd.backgroundOpacity || 1.0)
|
|
border.color: Qt.alpha(Color.mOutline, Settings.data.osd.backgroundOpacity || 1.0)
|
|
border.width: {
|
|
const bw = Math.max(2, Style.borderM)
|
|
return bw % 2 === 0 ? bw : bw + 1
|
|
}
|
|
}
|
|
|
|
NDropShadows {
|
|
anchors.fill: background
|
|
source: background
|
|
autoPaddingEnabled: true
|
|
}
|
|
|
|
Loader {
|
|
id: contentLoader
|
|
anchors.fill: background
|
|
anchors.margins: Style.marginM
|
|
active: true
|
|
sourceComponent: panel.verticalMode ? verticalContent : horizontalContent
|
|
}
|
|
|
|
Component {
|
|
id: horizontalContent
|
|
RowLayout {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.margins: Style.marginL
|
|
spacing: Style.marginM
|
|
|
|
NIcon {
|
|
icon: root.getIcon()
|
|
color: root.getIconColor()
|
|
pointSize: Style.fontSizeXL
|
|
Layout.alignment: Qt.AlignVCenter
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationNormal
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: panel.barThickness
|
|
radius: Math.round(panel.barThickness / 2)
|
|
color: Color.mSurfaceVariant
|
|
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
width: parent.width * Math.min(1.0, root.getCurrentValue() / root.getMaxValue())
|
|
radius: parent.radius
|
|
color: root.getProgressColor()
|
|
|
|
Behavior on width {
|
|
NumberAnimation {
|
|
duration: Style.animationNormal
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationNormal
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NText {
|
|
text: root.getDisplayPercentage()
|
|
color: Color.mOnSurface
|
|
pointSize: Style.fontSizeS
|
|
family: Settings.data.ui.fontFixed
|
|
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
|
horizontalAlignment: Text.AlignRight
|
|
verticalAlignment: Text.AlignVCenter
|
|
Layout.preferredWidth: Math.round(50 * Style.uiScaleRatio)
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: verticalContent
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.topMargin: Style.marginL
|
|
anchors.bottomMargin: Style.marginL
|
|
spacing: Style.marginS
|
|
|
|
NText {
|
|
text: root.getDisplayPercentage()
|
|
color: Color.mOnSurface
|
|
pointSize: Style.fontSizeS
|
|
family: Settings.data.ui.fontFixed
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: Math.round(20 * Style.uiScaleRatio)
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
Rectangle {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
width: panel.barThickness
|
|
radius: Math.round(panel.barThickness / 2)
|
|
color: Color.mSurfaceVariant
|
|
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
height: parent.height * Math.min(1.0, root.getCurrentValue() / root.getMaxValue())
|
|
radius: parent.radius
|
|
color: root.getProgressColor()
|
|
|
|
Behavior on height {
|
|
NumberAnimation {
|
|
duration: Style.animationNormal
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationNormal
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NIcon {
|
|
icon: root.getIcon()
|
|
color: root.getIconColor()
|
|
pointSize: Style.fontSizeL
|
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationNormal
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function show() {
|
|
hideTimer.stop()
|
|
visibilityTimer.stop()
|
|
osdItem.visible = true
|
|
|
|
Qt.callLater(() => {
|
|
osdItem.opacity = 1
|
|
osdItem.scale = 1.0
|
|
})
|
|
|
|
hideTimer.start()
|
|
}
|
|
|
|
function hide() {
|
|
hideTimer.stop()
|
|
visibilityTimer.stop()
|
|
osdItem.opacity = 0
|
|
osdItem.scale = 0.85
|
|
visibilityTimer.start()
|
|
}
|
|
|
|
function hideImmediately() {
|
|
hideTimer.stop()
|
|
visibilityTimer.stop()
|
|
osdItem.opacity = 0
|
|
osdItem.scale = 0.85
|
|
osdItem.visible = false
|
|
root.currentOSDType = ""
|
|
root.active = false
|
|
}
|
|
}
|
|
|
|
function showOSD() {
|
|
osdItem.show()
|
|
}
|
|
}
|
|
}
|
|
} |