diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 920bb69a..d040ed1b 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -70,7 +70,8 @@ "forceBlackScreenCorners": false, "radiusRatio": 1, "screenRadiusRatio": 1, - "animationSpeed": 1 + "animationSpeed": 1, + "showOSD": true }, "location": { "name": "Tokyo", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 14245f97..a49c029b 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -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 diff --git a/Modules/ControlCenter/Cards/MediaCard.qml b/Modules/ControlCenter/Cards/MediaCard.qml index f5fa1889..5e4ad4f8 100644 --- a/Modules/ControlCenter/Cards/MediaCard.qml +++ b/Modules/ControlCenter/Cards/MediaCard.qml @@ -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 { } } } -} \ No newline at end of file +} diff --git a/Modules/OSD/BrightnessOSD.qml b/Modules/OSD/BrightnessOSD.qml new file mode 100644 index 00000000..c2d5e3fb --- /dev/null +++ b/Modules/OSD/BrightnessOSD.qml @@ -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() + } + } +} diff --git a/Modules/OSD/VolumeOSD.qml b/Modules/OSD/VolumeOSD.qml new file mode 100644 index 00000000..1c28291c --- /dev/null +++ b/Modules/OSD/VolumeOSD.qml @@ -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() + } + } +} diff --git a/Modules/Settings/Tabs/GeneralTab.qml b/Modules/Settings/Tabs/GeneralTab.qml index 0e0d8932..add48277 100644 --- a/Modules/Settings/Tabs/GeneralTab.qml +++ b/Modules/Settings/Tabs/GeneralTab.qml @@ -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 diff --git a/shell.qml b/shell.qml index e7f53d16..cdf774f2 100644 --- a/shell.qml +++ b/shell.qml @@ -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 {}