OSD: Single component instance. Multi monitor support (follows notifications settings)

This commit is contained in:
ItsLemmy
2025-09-24 17:05:57 -04:00
parent b96deaa0c3
commit 22b8edb023
2 changed files with 311 additions and 272 deletions
+310 -257
View File
@@ -7,314 +7,367 @@ import qs.Commons
import qs.Services
import qs.Widgets
Loader {
id: windowLoader
active: false
// Unified OSD component - handles both volume and brightness with a single instance
// Loader activates only when showing OSD, deactivates when hidden to save resources
Variants {
model: Quickshell.screens
// OSD Type enum
enum Type {
Volume,
Brightness
}
delegate: Loader {
id: root
property int osdType: OSD.Type.Volume
readonly property real scaling: ScalingService.getScreenScale(Quickshell.screens[0])
required property ShellScreen modelData
property real scaling: ScalingService.getScreenScale(modelData)
// Volume properties
readonly property real currentVolume: AudioService.volume
readonly property bool isMuted: AudioService.muted
property bool firstVolumeReceived: false
property bool firstMuteReceived: false
// Access the notification model from the service
property ListModel notificationModel: NotificationService.activeList
// Brightness properties
readonly property real currentBrightness: {
if (BrightnessService.monitors.length > 0) {
return BrightnessService.monitors[0].brightness || 0
}
return 0
}
property bool firstBrightnessReceived: false
// If no notification display activated in settings, then show them all
property bool canShowOnThisScreen: Settings.isLoaded && modelData && (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0))
// Get appropriate icon based on OSD type
function getIcon() {
if (osdType === OSD.Type.Volume) {
if (AudioService.muted) {
return "volume-mute"
// Loader is only active when actually showing something
active: false
// Current OSD display state
property string currentOSDType: "" // "volume", "brightness", or ""
// Volume properties
readonly property real currentVolume: AudioService.volume
readonly property bool isMuted: AudioService.muted
property bool volumeInitialized: false
property bool muteInitialized: false
// Brightness properties
property bool brightnessInitialized: false
readonly property real currentBrightness: {
if (BrightnessService.monitors.length > 0) {
return BrightnessService.monitors[0].brightness || 0
}
return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
} else {
// Brightness
var brightness = currentBrightness
return brightness <= 0.5 ? "brightness-low" : "brightness-high"
return 0
}
}
// Get current value (0-1 range)
function getCurrentValue() {
if (osdType === OSD.Type.Volume) {
return isMuted ? 0 : currentVolume
} else {
return currentBrightness
// Get appropriate icon based on current OSD type
function getIcon() {
if (currentOSDType === "volume") {
if (AudioService.muted) {
return "volume-mute"
}
return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
} else if (currentOSDType === "brightness") {
return currentBrightness <= 0.5 ? "brightness-low" : "brightness-high"
}
return ""
}
}
// Get display percentage
function getDisplayPercentage() {
if (osdType === OSD.Type.Volume) {
return isMuted ? "0%" : Math.round(currentVolume * 100) + "%"
} else {
return Math.round(currentBrightness * 100) + "%"
// Get current value (0-1 range)
function getCurrentValue() {
if (currentOSDType === "volume") {
return isMuted ? 0 : currentVolume
} else if (currentOSDType === "brightness") {
return currentBrightness
}
return 0
}
}
// Get progress bar color
function getProgressColor() {
if (osdType === OSD.Type.Volume) {
if (isMuted)
return Color.mError
if (currentVolume > 1.0)
return Color.mError
return Color.mPrimary
} else {
// Get display percentage
function getDisplayPercentage() {
if (currentOSDType === "volume") {
return isMuted ? "0%" : Math.round(currentVolume * 100) + "%"
} else if (currentOSDType === "brightness") {
return Math.round(currentBrightness * 100) + "%"
}
return ""
}
// Get progress bar color
function getProgressColor() {
if (currentOSDType === "volume") {
if (isMuted)
return Color.mError
if (currentVolume > 1.0)
return Color.mError
return Color.mPrimary
}
return Color.mPrimary
}
}
// Get icon color
function getIconColor() {
if (osdType === OSD.Type.Volume) {
return isMuted ? Color.mError : Color.mOnSurface
} else {
// Get icon color
function getIconColor() {
if (currentOSDType === "volume" && isMuted) {
return Color.mError
}
return Color.mOnSurface
}
}
sourceComponent: PanelWindow {
id: panel
sourceComponent: PanelWindow {
screen: modelData
screen: Quickshell.screens[0] // Use primary screen
anchors {
top: true
}
implicitWidth: 320 * windowLoader.scaling
implicitHeight: osdItem.height
// Set margins based on bar position
margins.top: {
switch (Settings.data.bar.position) {
case "top":
return (Style.barHeight + Style.marginS) * windowLoader.scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * windowLoader.scaling : 0)
default:
return Style.marginL * windowLoader.scaling
anchors {
top: true
}
}
color: Color.transparent
implicitWidth: 320 * root.scaling
implicitHeight: osdItem.height
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: PanelWindow.ExclusionMode.Ignore
Rectangle {
id: osdItem
width: parent.width
height: Math.round(contentLayout.implicitHeight + Style.marginL * 2 * windowLoader.scaling)
radius: Style.radiusL * windowLoader.scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(2, Style.borderM * windowLoader.scaling)
visible: false
opacity: 0
scale: 0.7
anchors.horizontalCenter: parent.horizontalCenter
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
// Set margins based on bar position
margins.top: {
switch (Settings.data.bar.position) {
case "top":
return (Style.barHeight + Style.marginS) * root.scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * root.scaling : 0)
default:
return Style.marginL * root.scaling
}
}
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
color: Color.transparent
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: PanelWindow.ExclusionMode.Ignore
Rectangle {
id: osdItem
width: parent.width
height: Math.round(contentLayout.implicitHeight + Style.marginL * 2 * root.scaling)
radius: Style.radiusL * root.scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(2, Style.borderM * root.scaling)
visible: false
opacity: 0
scale: 0.85
anchors.horizontalCenter: parent.horizontalCenter
Behavior on opacity {
NumberAnimation {
id: opacityAnimation
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
}
Timer {
id: hideTimer
interval: 2000
onTriggered: osdItem.hide()
}
Behavior on scale {
NumberAnimation {
id: scaleAnimation
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
RowLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginM * windowLoader.scaling
spacing: Style.marginM * windowLoader.scaling
Timer {
id: hideTimer
interval: 2000
onTriggered: osdItem.hide()
}
NIcon {
icon: windowLoader.getIcon()
color: windowLoader.getIconColor()
font.pointSize: Style.fontSizeXL * windowLoader.scaling
Layout.alignment: Qt.AlignVCenter
// Timer to handle visibility after animations complete
Timer {
id: visibilityTimer
interval: Style.animationNormal + 50 // Add small buffer
onTriggered: {
osdItem.visible = false
root.currentOSDType = ""
// Deactivate the loader when done
root.active = false
}
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginXS * windowLoader.scaling
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginM * root.scaling
spacing: Style.marginM * root.scaling
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.round(6 * windowLoader.scaling)
radius: Math.round(3 * windowLoader.scaling)
color: Color.mSurfaceVariant
NIcon {
icon: root.getIcon()
color: root.getIconColor()
font.pointSize: Style.fontSizeXL * root.scaling
Layout.alignment: Qt.AlignVCenter
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * Math.min(1.0, windowLoader.getCurrentValue())
radius: parent.radius
color: windowLoader.getProgressColor()
Behavior on width {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
// Smooth icon transitions
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
}
NText {
text: windowLoader.getDisplayPercentage()
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * windowLoader.scaling
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: Math.round(32 * windowLoader.scaling)
spacing: Style.marginXS * root.scaling
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.round(6 * root.scaling)
radius: Math.round(3 * root.scaling)
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())
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
font.pointSize: Style.fontSizeS * root.scaling
Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: Math.round(32 * root.scaling)
}
}
}
function show() {
// Cancel any pending hide operations
hideTimer.stop()
visibilityTimer.stop()
// Make visible and animate in
osdItem.visible = true
// Use Qt.callLater to ensure the visible change is processed before animation
Qt.callLater(function () {
osdItem.opacity = 1
osdItem.scale = 1.0
})
// Start the auto-hide timer
hideTimer.start()
}
function hide() {
hideTimer.stop()
visibilityTimer.stop()
// Start fade out animation
osdItem.opacity = 0
osdItem.scale = 0.85 // Less dramatic scale change for smoother effect
// Delay hiding the element until after animation completes
visibilityTimer.start()
}
function hideImmediately() {
hideTimer.stop()
visibilityTimer.stop()
osdItem.opacity = 0
osdItem.scale = 0.85
osdItem.visible = false
root.currentOSDType = ""
root.active = false
}
}
function show() {
hideTimer.stop()
osdItem.visible = true
osdItem.opacity = 1
osdItem.scale = 1.0
hideTimer.start()
function showOSD() {
osdItem.show()
}
}
// Volume change monitoring
Connections {
target: AudioService
function onVolumeChanged() {
if (!volumeInitialized) {
volumeInitialized = true
} else {
showOSD("volume")
}
}
function hide() {
hideTimer.stop()
osdItem.opacity = 0
osdItem.scale = 0.7
function onMutedChanged() {
if (!muteInitialized) {
muteInitialized = true
} else {
showOSD("volume")
}
}
}
// Brightness change monitoring
Connections {
target: BrightnessService
function onMonitorsChanged() {
connectBrightnessMonitors()
}
}
Component.onCompleted: {
connectBrightnessMonitors()
}
function connectBrightnessMonitors() {
for (var i = 0; i < BrightnessService.monitors.length; i++) {
let monitor = BrightnessService.monitors[i]
// Disconnect first to avoid duplicate connections
monitor.brightnessUpdated.disconnect(onBrightnessChanged)
monitor.brightnessUpdated.connect(onBrightnessChanged)
}
}
function onBrightnessChanged(newBrightness) {
if (!brightnessInitialized) {
brightnessInitialized = true
} else {
showOSD("brightness")
}
}
function showOSD(type) {
// Check if OSD is enabled in settings and can show on this screen
if (!Settings.data.notifications.enableOSD || !canShowOnThisScreen) {
return
}
// Update the current OSD type
currentOSDType = type
// Activate the loader if not already active
if (!root.active) {
root.active = true
}
// Show the OSD (may need to wait for loader to create the item)
if (root.item) {
root.item.showOSD()
} else {
// If item not ready yet, wait for it
Qt.callLater(function () {
osdItem.visible = false
windowLoader.active = false
if (root.item) {
root.item.showOSD()
}
})
}
}
function hideImmediately() {
hideTimer.stop()
osdItem.opacity = 0
osdItem.scale = 0.7
osdItem.visible = false
windowLoader.active = false
function hideOSD() {
if (root.item && root.item.osdItem) {
root.item.osdItem.hideImmediately()
} else if (root.active) {
// If loader is active but item isn't ready, just deactivate
root.active = false
}
}
function showOSD() {
osdItem.show()
}
}
// Volume change monitoring
Connections {
target: AudioService
enabled: osdType === OSD.Type.Volume
function onVolumeChanged() {
if (!firstVolumeReceived) {
firstVolumeReceived = true
} else {
showOSD()
}
}
function onMutedChanged() {
if (!firstMuteReceived) {
firstMuteReceived = true
} else {
showOSD()
}
}
}
// Brightness change monitoring
Connections {
target: BrightnessService
enabled: osdType === OSD.Type.Brightness
function onMonitorsChanged() {
for (var i = 0; i < BrightnessService.monitors.length; i++) {
let monitor = BrightnessService.monitors[i]
monitor.brightnessUpdated.connect(windowLoader.onBrightnessChanged)
}
}
}
Component.onCompleted: {
if (osdType === OSD.Type.Brightness) {
for (var i = 0; i < BrightnessService.monitors.length; i++) {
let monitor = BrightnessService.monitors[i]
monitor.brightnessUpdated.connect(windowLoader.onBrightnessChanged)
}
}
}
function onBrightnessChanged(newBrightness) {
if (!firstBrightnessReceived) {
firstBrightnessReceived = true
} else {
showOSD()
}
}
// Signal to coordinate with other OSDs
signal osdShowing
function showOSD() {
// Check if OSD is enabled in settings
if (!Settings.data.notifications.enableOSD) {
return
}
osdShowing() // Notify other OSDs to hide
windowLoader.active = true
if (windowLoader.item) {
windowLoader.item.showOSD()
}
}
function hideOSD() {
if (windowLoader.item && windowLoader.item.osdItem) {
windowLoader.item.osdItem.hideImmediately()
} else if (windowLoader.active) {
// If window exists but osdItem isn't ready, just deactivate the loader
windowLoader.active = false
}
}
}
+1 -15
View File
@@ -59,21 +59,7 @@ ShellRoot {
}
ToastOverlay {}
// OSD overlays for volume and brightness
OSD {
id: volumeOSD
objectName: "volumeOSD"
osdType: OSD.Type.Volume
onOsdShowing: brightnessOSD.hideOSD()
}
OSD {
id: brightnessOSD
objectName: "brightnessOSD"
osdType: OSD.Type.Brightness
onOsdShowing: volumeOSD.hideOSD()
}
OSD {}
// IPCService is treated as a service
// but it's actually an Item that needs to exists in the shell.