OSD: Implement Volume & Brightness OSD

This commit is contained in:
Ly-sec
2025-09-23 17:53:24 +02:00
parent 120ed36deb
commit 1ca84bf052
7 changed files with 470 additions and 11 deletions
+2 -1
View File
@@ -70,7 +70,8 @@
"forceBlackScreenCorners": false,
"radiusRatio": 1,
"screenRadiusRatio": 1,
"animationSpeed": 1
"animationSpeed": 1,
"showOSD": true
},
"location": {
"name": "Tokyo",
+1
View File
@@ -177,6 +177,7 @@ Singleton {
property real radiusRatio: 1.0
property real screenRadiusRatio: 1.0
property real animationSpeed: 1.0
property bool showOSD: true
}
// location
+10 -10
View File
@@ -32,12 +32,12 @@ NBox {
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
// NText {
// text: "No media player detected"
// color: Color.mOnSurfaceVariant
// Layout.alignment: Qt.AlignHCenter
// }
Item {
Layout.fillWidth: true
Layout.fillHeight: true
@@ -65,7 +65,7 @@ NBox {
RowLayout {
anchors.fill: parent
spacing: Style.marginS * scaling
NIcon {
icon: "caret-down"
font.pointSize: Style.fontSizeXXL * scaling
@@ -92,12 +92,12 @@ NBox {
var players = MediaService.getAvailablePlayers()
for (var i = 0; i < players.length; i++) {
menuItems.push({
label: players[i].identity,
action: i.toString(),
icon: "disc",
enabled: true,
visible: true
})
"label": players[i].identity,
"action": i.toString(),
"icon": "disc",
"enabled": true,
"visible": true
})
}
playerContextMenu.model = menuItems
playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height)
@@ -109,7 +109,7 @@ NBox {
parent: root
width: 200 * scaling
onTriggered: function(action) {
onTriggered: function (action) {
var index = parseInt(action)
if (!isNaN(index)) {
MediaService.selectedPlayerIndex = index
@@ -342,4 +342,4 @@ NBox {
}
}
}
}
}
+219
View File
@@ -0,0 +1,219 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
Loader {
id: windowLoader
active: false
readonly property real scaling: ScalingService.getScreenScale(Quickshell.screens[0])
readonly property real currentBrightness: {
if (BrightnessService.monitors.length > 0) {
return BrightnessService.monitors[0].brightness || 0
}
return 0
}
// Used to avoid showing OSD on Quickshell startup
property bool firstBrightnessReceived: false
function getIcon() {
var brightness = currentBrightness
return brightness <= 0.5 ? "brightness-low" : "brightness-high"
}
sourceComponent: PanelWindow {
id: panel
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
}
}
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 * 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
}
}
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
Timer {
id: hideTimer
interval: 2000
onTriggered: osdItem.hide()
}
RowLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginM * windowLoader.scaling
spacing: Style.marginM * windowLoader.scaling
NIcon {
icon: windowLoader.getIcon()
color: Color.mOnSurface
font.pointSize: Style.fontSizeXL * windowLoader.scaling
Layout.alignment: Qt.AlignVCenter
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginXS * windowLoader.scaling
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.round(6 * windowLoader.scaling)
radius: Math.round(3 * windowLoader.scaling)
color: Color.mSurfaceVariant
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * windowLoader.currentBrightness
radius: parent.radius
color: Color.mPrimary
Behavior on width {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
}
NText {
text: Math.round(windowLoader.currentBrightness * 100) + "%"
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * windowLoader.scaling
Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: Math.round(32 * windowLoader.scaling)
}
}
}
function show() {
hideTimer.stop()
osdItem.visible = true
osdItem.opacity = 1
osdItem.scale = 1.0
hideTimer.start()
}
function hide() {
hideTimer.stop()
osdItem.opacity = 0
osdItem.scale = 0.7
Qt.callLater(function () {
osdItem.visible = false
windowLoader.active = false
})
}
}
function showOSD() {
osdItem.show()
}
}
// Monitor brightness changes from all monitors
Connections {
target: BrightnessService
function onMonitorsChanged() {
// Connect to brightness changes for each monitor
for (var i = 0; i < BrightnessService.monitors.length; i++) {
let monitor = BrightnessService.monitors[i]
monitor.brightnessUpdated.connect(windowLoader.onBrightnessChanged)
}
}
}
// Connect to existing monitors on component completion
Component.onCompleted: {
for (var i = 0; i < BrightnessService.monitors.length; i++) {
let monitor = BrightnessService.monitors[i]
monitor.brightnessUpdated.connect(windowLoader.onBrightnessChanged)
}
}
function onBrightnessChanged(newBrightness) {
if (!firstBrightnessReceived) {
// Ignore the first brightness change on startup
firstBrightnessReceived = true
} else {
showOSD()
}
}
// Signal to coordinate with other OSDs
signal osdShowing
function showOSD() {
// Check if OSD is enabled in settings
if (!Settings.data.general.showOSD) {
return
}
osdShowing() // Notify other OSDs to hide
windowLoader.active = true
if (windowLoader.item) {
windowLoader.item.showOSD()
}
}
function hideOSD() {
if (windowLoader.item) {
windowLoader.item.osdItem.hideImmediately()
}
}
}
+217
View File
@@ -0,0 +1,217 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
Loader {
id: windowLoader
active: false
readonly property real currentVolume: AudioService.volume
readonly property bool isMuted: AudioService.muted
readonly property real scaling: ScalingService.getScreenScale(Quickshell.screens[0])
// Used to avoid showing OSD on Quickshell startup
property bool firstVolumeReceived: false
property bool firstMuteReceived: false
function getIcon() {
if (AudioService.muted) {
return "volume-mute"
}
return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
}
sourceComponent: PanelWindow {
id: panel
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
}
}
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 * 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
}
}
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
Timer {
id: hideTimer
interval: 2000
onTriggered: osdItem.hide()
}
RowLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginM * windowLoader.scaling
spacing: Style.marginM * windowLoader.scaling
NIcon {
icon: windowLoader.getIcon()
color: windowLoader.isMuted ? Color.mError : Color.mOnSurface
font.pointSize: Style.fontSizeXL * windowLoader.scaling
Layout.alignment: Qt.AlignVCenter
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginXS * windowLoader.scaling
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.round(6 * windowLoader.scaling)
radius: Math.round(3 * windowLoader.scaling)
color: Color.mSurfaceVariant
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * (windowLoader.isMuted ? 0 : Math.min(1.0, windowLoader.currentVolume))
radius: parent.radius
color: windowLoader.isMuted ? Color.mError : Color.mPrimary
Behavior on width {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
NText {
text: windowLoader.isMuted ? "0%" : Math.round(windowLoader.currentVolume * 100) + "%"
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * windowLoader.scaling
Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: Math.round(32 * windowLoader.scaling)
}
}
}
function show() {
hideTimer.stop()
osdItem.visible = true
osdItem.opacity = 1
osdItem.scale = 1.0
hideTimer.start()
}
function hide() {
hideTimer.stop()
osdItem.opacity = 0
osdItem.scale = 0.7
Qt.callLater(function () {
osdItem.visible = false
windowLoader.active = false
})
}
}
function showOSD() {
osdItem.show()
}
}
// Monitor volume changes
Connections {
target: AudioService
function onVolumeChanged() {
if (!firstVolumeReceived) {
// Ignore the first volume change on startup
firstVolumeReceived = true
} else {
showOSD()
}
}
function onMutedChanged() {
if (!firstMuteReceived) {
// Ignore the first mute state change on startup
firstMuteReceived = true
} else {
showOSD()
}
}
}
// Signal to coordinate with other OSDs
signal osdShowing
function showOSD() {
// Check if OSD is enabled in settings
if (!Settings.data.general.showOSD) {
return
}
osdShowing() // Notify other OSDs to hide
windowLoader.active = true
if (windowLoader.item) {
windowLoader.item.showOSD()
}
}
function hideOSD() {
if (windowLoader.item) {
windowLoader.item.osdItem.hideImmediately()
}
}
}
+7
View File
@@ -76,6 +76,13 @@ ColumnLayout {
onToggled: checked => Settings.data.general.dimDesktop = checked
}
NToggle {
label: "Show volume and brightness OSD"
description: "Display on-screen notifications when adjusting volume or brightness."
checked: Settings.data.general.showOSD
onToggled: checked => Settings.data.general.showOSD = checked
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
+14
View File
@@ -36,6 +36,7 @@ import qs.Modules.Bar.WiFi
import qs.Modules.ControlCenter
import qs.Modules.Launcher
import qs.Modules.Notification
import qs.Modules.OSD
import qs.Modules.Settings
import qs.Modules.Toast
import qs.Modules.Wallpaper
@@ -59,6 +60,19 @@ ShellRoot {
ToastOverlay {}
// OSD overlays for volume and brightness
VolumeOSD {
id: volumeOSD
objectName: "volumeOSD"
onOsdShowing: brightnessOSD.hideOSD()
}
BrightnessOSD {
id: brightnessOSD
objectName: "brightnessOSD"
onOsdShowing: volumeOSD.hideOSD()
}
// IPCService is treated as a service
// but it's actually an Item that needs to exists in the shell.
IPCService {}