diff --git a/modules/notch/Notch.qml b/modules/notch/Notch.qml index 1ac441ed..43a0fab1 100644 --- a/modules/notch/Notch.qml +++ b/modules/notch/Notch.qml @@ -19,6 +19,7 @@ Item { property Component powermenuViewComponent property Component toolsMenuViewComponent property Component notificationViewComponent + property Component osdViewComponent property var stackView: stackViewInternal property bool isExpanded: stackViewInternal.depth > 1 property bool isHovered: false diff --git a/modules/notch/NotchWindow.qml b/modules/notch/NotchWindow.qml index 29023875..01b3ffc2 100644 --- a/modules/notch/NotchWindow.qml +++ b/modules/notch/NotchWindow.qml @@ -14,6 +14,7 @@ import qs.modules.services import qs.modules.components import qs.modules.widgets.launcher import qs.config +import qs.modules.widgets.osd import "./NotchNotificationView.qml" PanelWindow { @@ -80,6 +81,7 @@ PanelWindow { // Hover state with delay to prevent flickering property bool hoverActive: false + property bool showingOSD: false // Track if mouse is over any notch-related area readonly property bool isMouseOverNotch: notchMouseAreaHover.hovered || notchRegionHover.hovered @@ -91,7 +93,7 @@ PanelWindow { // Show on interaction (hover, open, notifications) // This works even in fullscreen, ensuring hover always works - if (screenNotchOpen || hasActiveNotifications || hoverActive || barHoverActive) { + if (screenNotchOpen || hasActiveNotifications || hoverActive || barHoverActive || showingOSD) { return true; } @@ -166,40 +168,184 @@ PanelWindow { // Default view component - user@host text Component { - id: defaultViewComponent + id: defaultViewComp DefaultView {} } // Launcher view component Component { - id: launcherViewComponent + id: launcherViewComp LauncherView {} } // Dashboard view component Component { - id: dashboardViewComponent + id: dashboardViewComp DashboardView {} } // Power menu view component Component { - id: powermenuViewComponent + id: powermenuViewComp PowerMenuView {} } // Tools menu view component Component { - id: toolsMenuViewComponent + id: toolsMenuViewComp ToolsMenuView {} } // Notification view component Component { - id: notificationViewComponent + id: notificationViewComp NotchNotificationView {} } + // OSD view component + Component { + id: osdViewComp + OSDView {} + } + + Timer { + id: osdTimer + interval: 2000 + repeat: false + onTriggered: { + if (notchPanel.isMouseOverNotch) { + osdTimer.restart(); + return; + } + + if (notchContainer.stackView.currentItem && notchContainer.stackView.currentItem.hasOwnProperty("showVolume")) { + notchContainer.stackView.pop(); + + // Back to default view + notchContainer.isShowingDefault = true; + notchContainer.isShowingNotifications = false; + notchPanel.showingOSD = false; + } + } + } + + property bool startupComplete: false + Timer { + id: startupTimer + interval: 3000 + running: true + repeat: false + onTriggered: { + console.log("[NotchWindow] Startup complete"); + notchPanel.startupComplete = true; + } + } + + function showOSD(type, value, icon) { + console.log("[NotchWindow] showOSD called with type:", type, "value:", value); + // Do not show OSD if notch is already open with a menu + if (notchPanel.screenNotchOpen) { + console.log("[NotchWindow] OSD skipped: screenNotchOpen is true"); + return; + } + + // Do not show OSD on startup + if (!notchPanel.startupComplete) { + console.log("[NotchWindow] OSD skipped: startup not complete"); + return; + } + + const currentItem = notchContainer.stackView.currentItem; + const isOSD = currentItem && currentItem.hasOwnProperty("showVolume"); // Check for OSDView property + console.log("[NotchWindow] Current item is OSD?", isOSD); + + if (isOSD) { + console.log("[NotchWindow] Updating existing OSD"); + // Update existing OSDView + if (type === "volume") { + currentItem.showVolume = true; + currentItem.volumeValue = value; + } else if (type === "mic") { + currentItem.showMic = true; + currentItem.micValue = value; + } else if (type === "brightness") { + currentItem.showBrightness = true; + currentItem.brightnessValue = value; + } + osdTimer.restart(); + notchPanel.showingOSD = true; + } else { + // Not current, push new OSDView + if (notchContainer.stackView.depth === 1) { + var props = { + showVolume: false, + showMic: false, + showBrightness: false, + volumeValue: 0, + micValue: 0, + brightnessValue: 0, + onKeepAlive: () => osdTimer.restart() + }; + + if (type === "volume") { + props.showVolume = true; + props.volumeValue = value; + } else if (type === "mic") { + props.showMic = true; + props.micValue = value; + } else if (type === "brightness") { + props.showBrightness = true; + props.brightnessValue = value; + } + + notchContainer.stackView.push(osdViewComp, props); + notchContainer.isShowingDefault = false; + osdTimer.restart(); + notchPanel.showingOSD = true; + } + } + } + + Connections { + target: Audio + function onValueChanged() { + console.log("[NotchWindow] Audio.onValueChanged received, value:", Audio.value); + showOSD("volume", Audio.value, Audio.volumeIcon(Audio.value, Audio.muted)); + } + function onMutedChanged() { + console.log("[NotchWindow] Audio.onMutedChanged received, muted:", Audio.muted); + showOSD("volume", Audio.value, Audio.volumeIcon(Audio.value, Audio.muted)); + } + } + + Connections { + target: Audio + function onMicValueChanged() { + console.log("[NotchWindow] Audio.onMicValueChanged received, value:", Audio.micValue); + showOSD("mic", Audio.micValue, Audio.micMuted ? Icons.microphoneOff : Icons.microphone); + } + function onMicMutedChanged() { + console.log("[NotchWindow] Audio.onMicMutedChanged received, muted:", Audio.micMuted); + showOSD("mic", Audio.micValue, Audio.micMuted ? Icons.microphoneOff : Icons.microphone); + } + } + + Connections { + target: Brightness + function onBrightnessChanged() { + // Find internal screen monitor + const monitor = Brightness.monitors.find(m => Brightness.isInternalScreen(m.screen) && m.screen.name === screen.name); + if (monitor) { + showOSD("brightness", monitor.brightness, Icons.sun); + } else { + // Fallback to any monitor if needed, or just the first one associated + if (Brightness.monitors.length > 0) { + showOSD("brightness", Brightness.monitors[0].brightness, Icons.sun); + } + } + } + } + // Hover region for detecting mouse when notch is hidden (doesn't block clicks) // Placed outside notchRegionContainer so it can work with mask independently Item { @@ -295,12 +441,13 @@ PanelWindow { layer.enabled: true layer.effect: Shadow {} - defaultViewComponent: defaultViewComponent - launcherViewComponent: launcherViewComponent - dashboardViewComponent: dashboardViewComponent - powermenuViewComponent: powermenuViewComponent - toolsMenuViewComponent: toolsMenuViewComponent - notificationViewComponent: notificationViewComponent + defaultViewComponent: defaultViewComp + launcherViewComponent: launcherViewComp + dashboardViewComponent: dashboardViewComp + powermenuViewComponent: powermenuViewComp + toolsMenuViewComponent: toolsMenuViewComp + notificationViewComponent: notificationViewComp + osdViewComponent: osdViewComp visibilities: screenVisibilities // Handle global keyboard events @@ -427,7 +574,7 @@ PanelWindow { function onLauncherChanged() { if (screenVisibilities.launcher) { - notchContainer.stackView.push(launcherViewComponent); + notchContainer.stackView.push(launcherViewComp); Qt.callLater(() => { if (notchContainer.stackView.currentItem) { notchContainer.stackView.currentItem.forceActiveFocus(); @@ -444,7 +591,7 @@ PanelWindow { function onDashboardChanged() { if (screenVisibilities.dashboard) { - notchContainer.stackView.push(dashboardViewComponent); + notchContainer.stackView.push(dashboardViewComp); Qt.callLater(() => { if (notchContainer.stackView.currentItem) { notchContainer.stackView.currentItem.forceActiveFocus(); @@ -461,7 +608,7 @@ PanelWindow { function onPowermenuChanged() { if (screenVisibilities.powermenu) { - notchContainer.stackView.push(powermenuViewComponent); + notchContainer.stackView.push(powermenuViewComp); Qt.callLater(() => { if (notchContainer.stackView.currentItem) { notchContainer.stackView.currentItem.forceActiveFocus(); @@ -478,7 +625,7 @@ PanelWindow { function onToolsChanged() { if (screenVisibilities.tools) { - notchContainer.stackView.push(toolsMenuViewComponent); + notchContainer.stackView.push(toolsMenuViewComp); Qt.callLater(() => { if (notchContainer.stackView.currentItem) { notchContainer.stackView.currentItem.forceActiveFocus(); diff --git a/modules/services/Audio.qml b/modules/services/Audio.qml index 683a5df2..aaa45d02 100644 --- a/modules/services/Audio.qml +++ b/modules/services/Audio.qml @@ -5,6 +5,7 @@ import QtQuick import Quickshell import Quickshell.Services.Pipewire import qs.modules.services +import qs.modules.theme /** * A nice wrapper for default Pipewire audio sink and source. @@ -19,6 +20,9 @@ Singleton { property PwNode source: Pipewire.defaultAudioSource readonly property real hardMaxValue: 2.00 property real value: sink?.audio?.volume ?? 0 + property bool muted: sink?.audio?.muted ?? false + property real micValue: source?.audio?.volume ?? 0 + property bool micMuted: source?.audio?.muted ?? false // Volume protection settings (persisted via StateService) property bool protectionEnabled: true diff --git a/modules/widgets/dashboard/widgets/FullPlayer.qml b/modules/widgets/dashboard/widgets/FullPlayer.qml index 1b7286b2..47e3e5cc 100644 --- a/modules/widgets/dashboard/widgets/FullPlayer.qml +++ b/modules/widgets/dashboard/widgets/FullPlayer.qml @@ -99,12 +99,37 @@ StyledRect { } } + Item { + anchors.fill: parent + visible: player.hasArtwork + + Image { + id: bgArt + anchors.fill: parent + source: MprisController.activePlayer?.trackArtUrl ?? "" + fillMode: Image.PreserveAspectCrop + visible: false + } + + MultiEffect { + anchors.fill: parent + source: bgArt + blurEnabled: true + blurMax: 64 + blur: 0.5 + opacity: 1 + saturation: 0.8 + } + } + + StyledRect { id: innerPlayer variant: "internalbg" anchors.fill: parent anchors.margins: 4 radius: player.radius - 4 + backgroundOpacity: 0.5 implicitHeight: mainLayout.implicitHeight + mainLayout.anchors.margins * 2 diff --git a/modules/widgets/osd/OSDView.qml b/modules/widgets/osd/OSDView.qml new file mode 100644 index 00000000..cb4512e7 --- /dev/null +++ b/modules/widgets/osd/OSDView.qml @@ -0,0 +1,237 @@ +import QtQuick +import QtQuick.Layouts +import qs.modules.theme +import qs.modules.components +import qs.modules.services +import qs.modules.notch +import qs.config + +Item { + id: root + + // Individual visibility flags (controlled by NotchWindow) + property bool showVolume: false + property bool showMic: false + property bool showBrightness: false + + // Values + property real volumeValue: 0 + property real micValue: 0 + property real brightnessValue: 0 + + onVolumeValueChanged: volumeSlider.value = volumeValue + onMicValueChanged: micSlider.value = micValue + onBrightnessValueChanged: brightnessSlider.value = brightnessValue + + property bool notchHovered: false + + // Layout constants + readonly property int padding: 16 + readonly property int osdHeight: Config.showBackground ? (Config.notchTheme === "island" ? 36 : 44) : (Config.notchTheme === "island" ? 36 : 40) + + // Notification constants + readonly property int notificationPadding: 16 + readonly property int notificationPaddingBottom: Config.notchTheme === "island" ? 20 : 16 + readonly property int notificationPaddingTop: 8 + readonly property bool hasActiveNotifications: Notifications.popupList.length > 0 + readonly property real notificationMinWidth: root.notchHovered ? 420 : 320 + readonly property real notificationContainerHeight: notificationView.implicitHeight + notificationPaddingTop + notificationPaddingBottom + + // Calculate total height of visible sliders + readonly property int visibleSlidersCount: (showVolume ? 1 : 0) + (showMic ? 1 : 0) + (showBrightness ? 1 : 0) + readonly property int slidersTotalHeight: visibleSlidersCount * osdHeight + + // Extra padding when multiple sliders are stacked (and no notification overrides it) + readonly property int multiSliderPadding: (visibleSlidersCount > 1 && !hasActiveNotifications) ? 6 : 0 + + // Dimensions + implicitHeight: (hasActiveNotifications ? notificationContainerHeight : 0) + slidersTotalHeight + multiSliderPadding + implicitWidth: Math.round(hasActiveNotifications ? Math.max(notificationMinWidth + (notificationPadding * 2), 300) : 300) + + Behavior on implicitWidth { + enabled: Config.animDuration > 0 + NumberAnimation { + duration: Config.animDuration + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } + } + + Behavior on implicitHeight { + enabled: Config.animDuration > 0 + NumberAnimation { + duration: Config.animDuration + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } + } + + Column { + anchors.fill: parent + spacing: 0 + + // --- VOLUME SLIDER --- + Item { + id: volumeContainer + width: parent.width + height: root.showVolume ? osdHeight : 0 + visible: root.showVolume + clip: true + + StyledSlider { + id: volumeSlider + anchors.fill: parent + anchors.margins: 4 + anchors.leftMargin: 12 + anchors.rightMargin: 18 + + // Binding + icon: Audio.volumeIcon(root.volumeValue, Audio.muted) + value: root.volumeValue + + enabled: true + iconPos: "start" + wavy: false + + backgroundColor: Colors.surfaceBright + + readonly property bool isMuted: Audio.muted + progressColor: isMuted ? Colors.surfaceBright : Colors.primary + + tooltip: false + + onValueChanged: { + if (Math.abs(value - root.volumeValue) > 0.005) { + Audio.setVolume(value); + root.keepAlive(); + } + } + } + + + Behavior on height { + enabled: Config.animDuration > 0 + NumberAnimation { duration: Config.animDuration; easing.type: Easing.OutQuart } + } + } + + // --- MIC SLIDER --- + Item { + id: micContainer + width: parent.width + height: root.showMic ? osdHeight : 0 + visible: root.showMic + clip: true + + StyledSlider { + id: micSlider + anchors.fill: parent + anchors.margins: 4 + anchors.leftMargin: 12 + anchors.rightMargin: 18 + + icon: Audio.micMuted ? Icons.micSlash : Icons.mic + value: root.micValue + + enabled: true + iconPos: "start" + wavy: false + + backgroundColor: Colors.surfaceBright + + readonly property bool isMuted: Audio.micMuted + progressColor: isMuted ? Colors.surfaceBright : Colors.secondary + + tooltip: false + + onValueChanged: { + if (Math.abs(value - root.micValue) > 0.005) { + Audio.setMicVolume(value); + root.keepAlive(); + } + } + } + + + Behavior on height { + enabled: Config.animDuration > 0 + NumberAnimation { duration: Config.animDuration; easing.type: Easing.OutQuart } + } + } + + // --- BRIGHTNESS SLIDER --- + Item { + id: brightnessContainer + width: parent.width + height: root.showBrightness ? osdHeight : 0 + visible: root.showBrightness + clip: true + + StyledSlider { + id: brightnessSlider + anchors.fill: parent + anchors.margins: 4 + anchors.leftMargin: 12 + anchors.rightMargin: 18 + + icon: Icons.sun + value: root.brightnessValue + + enabled: true + iconPos: "start" + wavy: false + + backgroundColor: Colors.surfaceBright + progressColor: Colors.tertiary + + tooltip: false + + onValueChanged: { + if (Math.abs(value - root.brightnessValue) > 0.005) { + Brightness.monitors.forEach(m => { + if (m.ready) m.setBrightness(value); + }); + root.keepAlive(); + } + } + } + + + Behavior on height { + enabled: Config.animDuration > 0 + NumberAnimation { duration: Config.animDuration; easing.type: Easing.OutQuart } + } + } + + // --- NOTIFICATIONS --- + Item { + id: notificationContainer + width: parent.width + height: hasActiveNotifications ? notificationContainerHeight : 0 + visible: hasActiveNotifications + clip: true + + NotchNotificationView { + id: notificationView + anchors.fill: parent + anchors.topMargin: notificationPaddingTop + anchors.leftMargin: notificationPadding + anchors.rightMargin: notificationPadding + anchors.bottomMargin: notificationPaddingBottom + visible: hasActiveNotifications + opacity: visible ? 1 : 0 + notchHovered: root.notchHovered + + Behavior on opacity { + enabled: Config.animDuration > 0 + NumberAnimation { + duration: Config.animDuration + easing.type: Easing.OutQuart + } + } + } + } + } + + signal keepAlive() +}