mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-08-15 18:43:59 +00:00
OSD: Implement Volume & Brightness OSD
This commit is contained in:
@@ -70,7 +70,8 @@
|
||||
"forceBlackScreenCorners": false,
|
||||
"radiusRatio": 1,
|
||||
"screenRadiusRatio": 1,
|
||||
"animationSpeed": 1
|
||||
"animationSpeed": 1,
|
||||
"showOSD": true
|
||||
},
|
||||
"location": {
|
||||
"name": "Tokyo",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user