diff --git a/modules/Backlight.qml b/modules/Backlight.qml index 0238759..f98a862 100644 --- a/modules/Backlight.qml +++ b/modules/Backlight.qml @@ -1,7 +1,6 @@ import QtQuick import Quickshell import Quickshell.Io -import Quickshell.Wayland import "." as M M.BarSection { @@ -12,9 +11,8 @@ M.BarSection { tooltip: "" property int percent: 0 - property bool _panelHovered: false property bool _osdActive: false - readonly property bool _showPanel: root._hovered || _panelHovered || _osdActive + readonly property bool _showPanel: root._hovered || hoverPanel.panelHovered || _osdActive onPercentChanged: if (percent > 0) _flashPanel() @@ -97,168 +95,83 @@ M.BarSection { onWheel: event => root.adjust(event.angleDelta.y) } - PanelWindow { - id: panel + M.HoverPanel { + id: hoverPanel + showPanel: root._showPanel + anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) + accentColor: root.accentColor + panelNamespace: "nova-backlight" + contentWidth: 200 - screen: QsWindow.window?.screen ?? null - visible: _winVisible - color: "transparent" + Item { + width: parent.width + height: 36 - property bool _winVisible: false - - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.exclusiveZone: 0 - WlrLayershell.namespace: "nova-backlight" - - anchors.top: true - anchors.left: true - - margins.top: 0 - margins.left: Math.max(0, Math.min(Math.round(root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) - implicitWidth / 2), (panel.screen?.width ?? 1920) - implicitWidth)) - - implicitWidth: panelContent.width - implicitHeight: panelContent.height - - Connections { - target: root - function on_ShowPanelChanged() { - if (root._showPanel) { - panel._winVisible = true; - hideAnim.stop(); - showAnim.start(); - } else { - showAnim.stop(); - hideAnim.start(); - } + Text { + id: blIcon + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "\uF185" + color: root.accentColor + font.pixelSize: M.Theme.fontSize + 2 + font.family: M.Theme.iconFontFamily } - } - - ParallelAnimation { - id: showAnim - NumberAnimation { - target: panelContent - property: "opacity" - to: 1 - duration: 120 - easing.type: Easing.OutCubic - } - NumberAnimation { - target: panelContent - property: "y" - to: 0 - duration: 150 - easing.type: Easing.OutCubic - } - } - - ParallelAnimation { - id: hideAnim - NumberAnimation { - target: panelContent - property: "opacity" - to: 0 - duration: 150 - easing.type: Easing.InCubic - } - NumberAnimation { - target: panelContent - property: "y" - to: -panelContent.height - duration: 150 - easing.type: Easing.InCubic - } - onFinished: panel._winVisible = false - } - - HoverHandler { - onHoveredChanged: root._panelHovered = hovered - } - - M.PopupBackground { - x: panelContent.x - y: panelContent.y - width: panelContent.width - height: panelContent.height - opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85) - accentColor: root.accentColor - } - - Column { - id: panelContent - width: 200 - opacity: 0 - y: -height Item { - width: parent.width - height: 36 + id: slider + anchors.left: blIcon.right + anchors.leftMargin: 8 + anchors.right: blLabel.left + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + height: 6 - Text { - id: blIcon - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "\uF185" + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 3 + } + + Rectangle { + width: parent.width * root.percent / 100 + height: parent.height color: root.accentColor - font.pixelSize: M.Theme.fontSize + 2 - font.family: M.Theme.iconFontFamily - } + radius: 3 - Item { - id: slider - anchors.left: blIcon.right - anchors.leftMargin: 8 - anchors.right: blLabel.left - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - height: 6 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 3 - } - - Rectangle { - width: parent.width * root.percent / 100 - height: parent.height - color: root.accentColor - radius: 3 - - Behavior on width { - NumberAnimation { - duration: 80 - } - } - } - - MouseArea { - anchors.fill: parent - anchors.margins: -6 - cursorShape: Qt.PointingHandCursor - onPressed: mouse => _set(mouse) - onPositionChanged: mouse => { - if (pressed) - _set(mouse); - } - function _set(mouse) { - root.setPercent(mouse.x / slider.width * 100); + Behavior on width { + NumberAnimation { + duration: 80 } } } - Text { - id: blLabel - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root.percent + "%" - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.fontFamily - width: 30 + MouseArea { + anchors.fill: parent + anchors.margins: -6 + cursorShape: Qt.PointingHandCursor + onPressed: mouse => _set(mouse) + onPositionChanged: mouse => { + if (pressed) + _set(mouse); + } + function _set(mouse) { + root.setPercent(mouse.x / slider.width * 100); + } } } + + Text { + id: blLabel + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root.percent + "%" + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.fontFamily + width: 30 + } } } } diff --git a/modules/HoverPanel.qml b/modules/HoverPanel.qml new file mode 100644 index 0000000..f7cb7a2 --- /dev/null +++ b/modules/HoverPanel.qml @@ -0,0 +1,115 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import "." as M + +// Shared hover/OSD panel PanelWindow — slides down from the bar on hover or +// external trigger. Parent module computes showPanel and reads panelHovered. +PanelWindow { + id: root + + required property bool showPanel + required property real anchorX + required property color accentColor + property string panelNamespace: "nova-panel" + property real contentWidth: 220 + property bool animateHeight: false + property bool panelHovered: false + + default property alias content: panelContent.children + + screen: QsWindow.window?.screen ?? null + visible: _winVisible + color: "transparent" + + property bool _winVisible: false + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.exclusiveZone: 0 + WlrLayershell.namespace: root.panelNamespace + + anchors.top: true + anchors.left: true + + margins.top: 0 + margins.left: Math.max(0, Math.min(Math.round(anchorX - implicitWidth / 2), (screen?.width ?? 1920) - implicitWidth)) + + implicitWidth: panelContent.width + implicitHeight: panelContent.height + + Behavior on implicitHeight { + enabled: root.animateHeight + NumberAnimation { + duration: 100 + easing.type: Easing.OutCubic + } + } + + onShowPanelChanged: { + if (showPanel) { + _winVisible = true; + hideAnim.stop(); + showAnim.start(); + } else { + showAnim.stop(); + hideAnim.start(); + } + } + + ParallelAnimation { + id: showAnim + NumberAnimation { + target: panelContent + property: "opacity" + to: 1 + duration: 120 + easing.type: Easing.OutCubic + } + NumberAnimation { + target: panelContent + property: "y" + to: 0 + duration: 150 + easing.type: Easing.OutCubic + } + } + + ParallelAnimation { + id: hideAnim + NumberAnimation { + target: panelContent + property: "opacity" + to: 0 + duration: 150 + easing.type: Easing.InCubic + } + NumberAnimation { + target: panelContent + property: "y" + to: -panelContent.height + duration: 150 + easing.type: Easing.InCubic + } + onFinished: root._winVisible = false + } + + HoverHandler { + onHoveredChanged: root.panelHovered = hovered + } + + M.PopupBackground { + x: panelContent.x + y: panelContent.y + width: panelContent.width + height: panelContent.height + opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85) + accentColor: root.accentColor + } + + Column { + id: panelContent + width: root.contentWidth + opacity: 0 + y: -height + } +} diff --git a/modules/Mpris.qml b/modules/Mpris.qml index 5f2d79c..fe15f2c 100644 --- a/modules/Mpris.qml +++ b/modules/Mpris.qml @@ -1,7 +1,6 @@ import QtQuick import Quickshell import Quickshell.Io -import Quickshell.Wayland import Quickshell.Services.Mpris import "." as M @@ -58,9 +57,8 @@ M.BarSection { required property var bar - property bool _panelHovered: false property bool _pinned: false - readonly property bool _anyHover: root._hovered || _panelHovered + readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered readonly property bool _showPanel: _anyHover || _pinned on_AnyHoverChanged: { @@ -97,383 +95,290 @@ M.BarSection { } } - PanelWindow { - id: panel + M.HoverPanel { + id: hoverPanel + showPanel: root._showPanel + anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) + accentColor: root.accentColor + panelNamespace: "nova-mpris" + contentWidth: 280 + animateHeight: true - screen: QsWindow.window?.screen ?? null - visible: _winVisible - color: "transparent" + // Album art + Item { + width: parent.width + height: _artImg._hasArt ? 140 : 60 + clip: true - property bool _winVisible: false - - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.exclusiveZone: 0 - WlrLayershell.namespace: "nova-mpris" - - anchors.top: true - anchors.left: true - - margins.top: 0 - margins.left: Math.max(0, Math.min(Math.round(root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) - implicitWidth / 2), (panel.screen?.width ?? 1920) - implicitWidth)) - - implicitWidth: panelContent.width - implicitHeight: panelContent.height - Behavior on implicitHeight { - NumberAnimation { - duration: 100 - easing.type: Easing.OutCubic + Rectangle { + anchors.fill: parent + color: M.Theme.base02 } - } - Connections { - target: root - function on_ShowPanelChanged() { - if (root._showPanel) { - panel._winVisible = true; - hideAnim.stop(); - showAnim.start(); - } else { - showAnim.stop(); - hideAnim.start(); - } - } - } + Image { + id: _artImg + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + visible: _hasArt + asynchronous: true + source: root._cachedArt - ParallelAnimation { - id: showAnim - NumberAnimation { - target: panelContent - property: "opacity" - to: 1 - duration: 120 - easing.type: Easing.OutCubic - } - NumberAnimation { - target: panelContent - property: "y" - to: 0 - duration: 150 - easing.type: Easing.OutCubic - } - } - - ParallelAnimation { - id: hideAnim - NumberAnimation { - target: panelContent - property: "opacity" - to: 0 - duration: 150 - easing.type: Easing.InCubic - } - NumberAnimation { - target: panelContent - property: "y" - to: -panelContent.height - duration: 150 - easing.type: Easing.InCubic - } - onFinished: panel._winVisible = false - } - - HoverHandler { - onHoveredChanged: root._panelHovered = hovered - } - - M.PopupBackground { - x: panelContent.x - y: panelContent.y - width: panelContent.width - height: panelContent.height - opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85) - accentColor: root.accentColor - } - - Column { - id: panelContent - width: 280 - opacity: 0 - y: -height - topPadding: 4 - bottomPadding: 4 - spacing: 2 - - // Album art - Item { - width: parent.width - height: _artImg._hasArt ? 140 : 60 - clip: true - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - } - - Image { - id: _artImg - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - visible: _hasArt - asynchronous: true - source: root._cachedArt - - property bool _hasArt: false - onStatusChanged: if (status === Image.Ready) - _hasArt = true - Connections { - target: root - function on_CachedArtChanged() { - if (!root._cachedArt) - _artImg._hasArt = false; - } + property bool _hasArt: false + onStatusChanged: if (status === Image.Ready) + _hasArt = true + Connections { + target: root + function on_CachedArtChanged() { + if (!root._cachedArt) + _artImg._hasArt = false; } } + } - // Visualizer bars - Row { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: parent.height * 0.6 - spacing: 2 - visible: root.playing - opacity: 0.5 + // Visualizer bars + Row { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height * 0.6 + spacing: 2 + visible: root.playing + opacity: 0.5 - Repeater { - model: 16 - Rectangle { - required property int index - width: (parent.width - 15 * parent.spacing) / 16 - height: parent.height * (root._cavaBars[index] ?? 0) - anchors.bottom: parent.bottom - color: root.accentColor - radius: 1 + Repeater { + model: 16 + Rectangle { + required property int index + width: (parent.width - 15 * parent.spacing) / 16 + height: parent.height * (root._cavaBars[index] ?? 0) + anchors.bottom: parent.bottom + color: root.accentColor + radius: 1 - Behavior on height { - NumberAnimation { - duration: 50 - } + Behavior on height { + NumberAnimation { + duration: 50 } } } } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 40 + visible: _artImg.visible + gradient: Gradient { + GradientStop { + position: 0 + color: "transparent" + } + GradientStop { + position: 1 + color: M.Theme.base01 + } + } + } + + Text { + anchors.centerIn: parent + text: "\uF001" + color: M.Theme.base04 + font.pixelSize: 28 + font.family: M.Theme.iconFontFamily + visible: _artImg.status !== Image.Ready + } + } + + // Track info + Item { + width: parent.width + height: titleCol.implicitHeight + 8 + + Column { + id: titleCol + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 2 + + Text { + width: parent.width + text: root.player?.trackTitle || "No track" + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize + 1 + font.family: M.Theme.fontFamily + font.bold: true + elide: Text.ElideRight + } + + Text { + width: parent.width + text: { + const p = root.player; + if (!p) + return ""; + const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || ""); + return [artist, p.trackAlbum].filter(s => s).join(" \u2014 "); + } + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + visible: text !== "" + } + } + } + + // Progress + Item { + width: parent.width + height: 20 + + readonly property real pos: root.player?.position ?? 0 + readonly property real dur: root.player?.length ?? 0 + readonly property real frac: dur > 0 ? pos / dur : 0 + + function _fmtTime(ms) { + const s = Math.floor(ms / 1000); + const m = Math.floor(s / 60); + return m + ":" + String(s % 60).padStart(2, "0"); + } + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: parent._fmtTime(parent.pos) + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: parent._fmtTime(parent.dur) + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + Item { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 80 + height: 4 Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 40 - visible: _artImg.visible - gradient: Gradient { - GradientStop { - position: 0 - color: "transparent" - } - GradientStop { - position: 1 - color: M.Theme.base01 - } - } + anchors.fill: parent + color: M.Theme.base02 + radius: 2 } - - Text { - anchors.centerIn: parent - text: "\uF001" - color: M.Theme.base04 - font.pixelSize: 28 - font.family: M.Theme.iconFontFamily - visible: _artImg.status !== Image.Ready + Rectangle { + width: parent.width * Math.min(1, Math.max(0, parent.parent.frac)) + height: parent.height + color: root.accentColor + radius: 2 } } + } - // Track info - Item { - width: parent.width - height: titleCol.implicitHeight + 8 + // Transport controls + Item { + width: parent.width + height: 36 - Column { - id: titleCol - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 2 - - Text { - width: parent.width - text: root.player?.trackTitle || "No track" - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize + 1 - font.family: M.Theme.fontFamily - font.bold: true - elide: Text.ElideRight - } - - Text { - width: parent.width - text: { - const p = root.player; - if (!p) - return ""; - const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || ""); - return [artist, p.trackAlbum].filter(s => s).join(" \u2014 "); - } - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - visible: text !== "" - } - } - } - - // Progress - Item { - width: parent.width - height: 20 - - readonly property real pos: root.player?.position ?? 0 - readonly property real dur: root.player?.length ?? 0 - readonly property real frac: dur > 0 ? pos / dur : 0 - - function _fmtTime(ms) { - const s = Math.floor(ms / 1000); - const m = Math.floor(s / 60); - return m + ":" + String(s % 60).padStart(2, "0"); - } - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: parent._fmtTime(parent.pos) - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: parent._fmtTime(parent.dur) - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - - Item { - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 80 - height: 4 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 2 - } - Rectangle { - width: parent.width * Math.min(1, Math.max(0, parent.parent.frac)) - height: parent.height - color: root.accentColor - radius: 2 - } - } - } - - // Transport controls - Item { - width: parent.width - height: 36 - - Row { - anchors.centerIn: parent - spacing: 24 - - Text { - text: "\uF048" - color: root.player?.canGoPrevious ? M.Theme.base05 : M.Theme.base03 - font.pixelSize: M.Theme.fontSize + 4 - font.family: M.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - enabled: root.player?.canGoPrevious ?? false - onClicked: root.player.previous() - } - } - - Text { - text: root.playing ? "\uF04C" : "\uF04B" - color: root.accentColor - font.pixelSize: M.Theme.fontSize + 8 - font.family: M.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: root.player?.togglePlaying() - } - } - - Text { - text: "\uF051" - color: root.player?.canGoNext ? M.Theme.base05 : M.Theme.base03 - font.pixelSize: M.Theme.fontSize + 4 - font.family: M.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - enabled: root.player?.canGoNext ?? false - onClicked: root.player.next() - } - } - } - } - - // Player switcher Row { - anchors.horizontalCenter: parent.horizontalCenter - height: 22 - spacing: 6 + anchors.centerIn: parent + spacing: 24 - Repeater { - model: root._players + Text { + text: "\uF048" + color: root.player?.canGoPrevious ? M.Theme.base05 : M.Theme.base03 + font.pixelSize: M.Theme.fontSize + 4 + font.family: M.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + enabled: root.player?.canGoPrevious ?? false + onClicked: root.player.previous() + } + } - delegate: Rectangle { - required property var modelData - required property int index + Text { + text: root.playing ? "\uF04C" : "\uF04B" + color: root.accentColor + font.pixelSize: M.Theme.fontSize + 8 + font.family: M.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.player?.togglePlaying() + } + } - readonly property bool _active: index === root._playerIdx + Text { + text: "\uF051" + color: root.player?.canGoNext ? M.Theme.base05 : M.Theme.base03 + font.pixelSize: M.Theme.fontSize + 4 + font.family: M.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + enabled: root.player?.canGoNext ?? false + onClicked: root.player.next() + } + } + } + } - width: _pLabel.implicitWidth + 12 - height: 18 - radius: 9 - color: _active ? M.Theme.base02 : (pArea.containsMouse ? M.Theme.base02 : "transparent") - border.color: _active ? root.accentColor : M.Theme.base03 - border.width: _active ? 1 : 0 - anchors.verticalCenter: parent.verticalCenter + // Player switcher + Row { + anchors.horizontalCenter: parent.horizontalCenter + height: 22 + spacing: 6 - Text { - id: _pLabel - anchors.centerIn: parent - text: modelData.identity ?? "Player" - color: _active ? root.accentColor : M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - font.bold: _active - } + Repeater { + model: root._players - MouseArea { - id: pArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root._playerIdx = index - } + delegate: Rectangle { + required property var modelData + required property int index + + readonly property bool _active: index === root._playerIdx + + width: _pLabel.implicitWidth + 12 + height: 18 + radius: 9 + color: _active ? M.Theme.base02 : (pArea.containsMouse ? M.Theme.base02 : "transparent") + border.color: _active ? root.accentColor : M.Theme.base03 + border.width: _active ? 1 : 0 + anchors.verticalCenter: parent.verticalCenter + + Text { + id: _pLabel + anchors.centerIn: parent + text: modelData.identity ?? "Player" + color: _active ? root.accentColor : M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + font.bold: _active + } + + MouseArea { + id: pArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root._playerIdx = index } } } diff --git a/modules/Volume.qml b/modules/Volume.qml index b2d9500..8f8bea5 100644 --- a/modules/Volume.qml +++ b/modules/Volume.qml @@ -1,6 +1,5 @@ import QtQuick import Quickshell -import Quickshell.Wayland import Quickshell.Services.Pipewire import "." as M @@ -40,11 +39,13 @@ M.BarSection { } property bool _expanded: false - property bool _panelHovered: false property bool _osdActive: false - readonly property bool _anyHover: root._hovered || _panelHovered + readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered readonly property bool _showPanel: _anyHover || _expanded || _osdActive + on_ShowPanelChanged: if (!_showPanel) + _expanded = false + onVolumeChanged: _flashPanel() onMutedChanged: _flashPanel() @@ -104,409 +105,320 @@ M.BarSection { } // Unified volume panel — hover shows slider, click expands to show devices - PanelWindow { - id: panel + M.HoverPanel { + id: hoverPanel + showPanel: root._showPanel + anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) + accentColor: root.accentColor + panelNamespace: "nova-volume" + contentWidth: 220 - screen: QsWindow.window?.screen ?? null - visible: _winVisible - color: "transparent" + // Compact: slider row + Item { + width: parent.width + height: 36 - property bool _winVisible: false + // Mute toggle + Text { + id: muteIcon + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._volumeIcon + color: root._volumeColor + font.pixelSize: M.Theme.fontSize + 2 + font.family: M.Theme.iconFontFamily - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.exclusiveZone: 0 - WlrLayershell.namespace: "nova-volume" - - anchors.top: true - anchors.left: true - - margins.top: 0 - margins.left: Math.max(0, Math.min(Math.round(root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) - implicitWidth / 2), (panel.screen?.width ?? 1920) - implicitWidth)) - - implicitWidth: panelContent.width - implicitHeight: panelContent.height - - // Show/hide logic - Connections { - target: root - function on_ShowPanelChanged() { - if (root._showPanel) { - panel._winVisible = true; - hideAnim.stop(); - showAnim.start(); - } else { - root._expanded = false; - showAnim.stop(); - hideAnim.start(); + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: if (root.sink?.audio) + root.sink.audio.muted = !root.sink.audio.muted } } - } - ParallelAnimation { - id: showAnim - NumberAnimation { - target: panelContent - property: "opacity" - to: 1 - duration: 120 - easing.type: Easing.OutCubic - } - NumberAnimation { - target: panelContent - property: "y" - to: 0 - duration: 150 - easing.type: Easing.OutCubic - } - } - - ParallelAnimation { - id: hideAnim - NumberAnimation { - target: panelContent - property: "opacity" - to: 0 - duration: 150 - easing.type: Easing.InCubic - } - NumberAnimation { - target: panelContent - property: "y" - to: -panelContent.height - duration: 150 - easing.type: Easing.InCubic - } - onFinished: panel._winVisible = false - } - - // Keep panel open when mouse is over it - HoverHandler { - onHoveredChanged: root._panelHovered = hovered - // Click inside panel doesn't dismiss - } - - M.PopupBackground { - x: panelContent.x - y: panelContent.y - width: panelContent.width - height: panelContent.height - opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85) - accentColor: root.accentColor - } - - Column { - id: panelContent - width: 220 - opacity: 0 - y: -height - - // Compact: slider row + // Slider Item { - width: parent.width - height: 36 + id: slider + anchors.left: muteIcon.right + anchors.leftMargin: 8 + anchors.right: volLabel.left + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + height: 6 - // Mute toggle - Text { - id: muteIcon - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._volumeIcon + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 3 + } + + Rectangle { + width: parent.width * Math.min(1, Math.max(0, root.volume)) + height: parent.height color: root._volumeColor - font.pixelSize: M.Theme.fontSize + 2 - font.family: M.Theme.iconFontFamily + radius: 3 - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: if (root.sink?.audio) - root.sink.audio.muted = !root.sink.audio.muted - } - } - - // Slider - Item { - id: slider - anchors.left: muteIcon.right - anchors.leftMargin: 8 - anchors.right: volLabel.left - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - height: 6 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 3 - } - - Rectangle { - width: parent.width * Math.min(1, Math.max(0, root.volume)) - height: parent.height - color: root._volumeColor - radius: 3 - - Behavior on width { - NumberAnimation { - duration: 80 - } - } - } - - MouseArea { - anchors.fill: parent - anchors.margins: -6 - cursorShape: Qt.PointingHandCursor - onPressed: mouse => _setVol(mouse) - onPositionChanged: mouse => { - if (pressed) - _setVol(mouse); - } - function _setVol(mouse) { - if (!root.sink?.audio) - return; - root.sink.audio.volume = Math.max(0, Math.min(1, mouse.x / slider.width)); + Behavior on width { + NumberAnimation { + duration: 80 } } } - Text { - id: volLabel - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: Math.round(root.volume * 100) + "%" - color: root.muted ? M.Theme.base04 : M.Theme.base05 - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.fontFamily - width: 30 + MouseArea { + anchors.fill: parent + anchors.margins: -6 + cursorShape: Qt.PointingHandCursor + onPressed: mouse => _setVol(mouse) + onPositionChanged: mouse => { + if (pressed) + _setVol(mouse); + } + function _setVol(mouse) { + if (!root.sink?.audio) + return; + root.sink.audio.volume = Math.max(0, Math.min(1, mouse.x / slider.width)); + } } } - // Sink name + Text { + id: volLabel + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: Math.round(root.volume * 100) + "%" + color: root.muted ? M.Theme.base04 : M.Theme.base05 + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.fontFamily + width: 30 + } + } + + // Sink name + Text { + width: parent.width + height: 18 + horizontalAlignment: Text.AlignHCenter + text: root.sink?.description ?? root.sink?.name ?? "" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + leftPadding: 12 + rightPadding: 12 + } + + // Expanded: output device list + Column { + id: deviceList + width: parent.width + visible: root._expanded + clip: true + + property real _targetHeight: root._expanded ? implicitHeight : 0 + height: _targetHeight + Behavior on height { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + + // Separator + Rectangle { + width: parent.width - 16 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + } + + // Header Text { width: parent.width - height: 18 - horizontalAlignment: Text.AlignHCenter - text: root.sink?.description ?? root.sink?.name ?? "" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - elide: Text.ElideRight + height: 24 + verticalAlignment: Text.AlignVCenter leftPadding: 12 - rightPadding: 12 + text: "Output Devices" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily } - // Expanded: output device list - Column { - id: deviceList - width: parent.width - visible: root._expanded - clip: true + Repeater { + model: root._sinkList - property real _targetHeight: root._expanded ? implicitHeight : 0 - height: _targetHeight - Behavior on height { - NumberAnimation { - duration: 200 - easing.type: Easing.OutCubic + delegate: Item { + required property var modelData + + width: deviceList.width + height: 28 + + readonly property bool _active: modelData === root.sink + + Rectangle { + anchors.fill: parent + anchors.leftMargin: 4 + anchors.rightMargin: 4 + color: deviceArea.containsMouse ? M.Theme.base02 : "transparent" + radius: M.Theme.radius + } + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.description || modelData.name || "Unknown" + color: parent._active ? root.accentColor : M.Theme.base05 + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.fontFamily + font.bold: parent._active + elide: Text.ElideRight + } + + MouseArea { + id: deviceArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Pipewire.preferredDefaultAudioSink = modelData } } + } + + // Streams header (only if there are streams) + Item { + visible: root._streamList.length > 0 + width: parent.width + height: visible ? streamSep.height + streamHeader.height : 0 - // Separator Rectangle { + id: streamSep width: parent.width - 16 height: 1 anchors.horizontalCenter: parent.horizontalCenter color: M.Theme.base03 } - // Header Text { + id: streamHeader + anchors.top: streamSep.bottom width: parent.width height: 24 verticalAlignment: Text.AlignVCenter leftPadding: 12 - text: "Output Devices" + text: "Applications" color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 1 font.family: M.Theme.fontFamily } + } - Repeater { - model: root._sinkList + Repeater { + model: root._streamList - delegate: Item { - required property var modelData + delegate: Item { + id: streamEntry + required property var modelData - width: deviceList.width - height: 28 + width: deviceList.width + height: 32 - readonly property bool _active: modelData === root.sink + readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown" + readonly property real _vol: modelData.audio?.volume ?? 0 + readonly property bool _muted: modelData.audio?.muted ?? false - Rectangle { - anchors.fill: parent - anchors.leftMargin: 4 - anchors.rightMargin: 4 - color: deviceArea.containsMouse ? M.Theme.base02 : "transparent" - radius: M.Theme.radius - } - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.description || modelData.name || "Unknown" - color: parent._active ? root.accentColor : M.Theme.base05 - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.fontFamily - font.bold: parent._active - elide: Text.ElideRight - } + Text { + id: streamIcon + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: streamEntry._muted ? "\uF026" : "\uF028" + color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.iconFontFamily MouseArea { - id: deviceArea anchors.fill: parent - hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: Pipewire.preferredDefaultAudioSink = modelData + onClicked: if (streamEntry.modelData.audio) + streamEntry.modelData.audio.muted = !streamEntry.modelData.audio.muted } } - } - - // Streams header (only if there are streams) - Item { - visible: root._streamList.length > 0 - width: parent.width - height: visible ? streamSep.height + streamHeader.height : 0 - - Rectangle { - id: streamSep - width: parent.width - 16 - height: 1 - anchors.horizontalCenter: parent.horizontalCenter - color: M.Theme.base03 - } Text { - id: streamHeader - anchors.top: streamSep.bottom - width: parent.width - height: 24 - verticalAlignment: Text.AlignVCenter - leftPadding: 12 - text: "Applications" - color: M.Theme.base04 + id: streamName + anchors.left: streamIcon.right + anchors.leftMargin: 6 + anchors.verticalCenter: parent.verticalCenter + text: streamEntry._appName + color: M.Theme.base05 font.pixelSize: M.Theme.fontSize - 1 font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: 70 } - } - Repeater { - model: root._streamList + Item { + id: streamSlider + anchors.left: streamName.right + anchors.leftMargin: 6 + anchors.right: streamVol.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + height: 4 - delegate: Item { - id: streamEntry - required property var modelData - - width: deviceList.width - height: 32 - - readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown" - readonly property real _vol: modelData.audio?.volume ?? 0 - readonly property bool _muted: modelData.audio?.muted ?? false - - Text { - id: streamIcon - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: streamEntry._muted ? "\uF026" : "\uF028" - color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.iconFontFamily - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: if (streamEntry.modelData.audio) - streamEntry.modelData.audio.muted = !streamEntry.modelData.audio.muted - } + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 2 + } + Rectangle { + width: parent.width * Math.min(1, Math.max(0, streamEntry._vol)) + height: parent.height + color: streamEntry._muted ? M.Theme.base04 : root.accentColor + radius: 2 } - Text { - id: streamName - anchors.left: streamIcon.right - anchors.leftMargin: 6 - anchors.verticalCenter: parent.verticalCenter - text: streamEntry._appName - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: 70 - } - - Item { - id: streamSlider - anchors.left: streamName.right - anchors.leftMargin: 6 - anchors.right: streamVol.left - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - height: 4 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 2 + MouseArea { + anchors.fill: parent + anchors.margins: -6 + cursorShape: Qt.PointingHandCursor + onPressed: mouse => _set(mouse) + onPositionChanged: mouse => { + if (pressed) + _set(mouse); } - Rectangle { - width: parent.width * Math.min(1, Math.max(0, streamEntry._vol)) - height: parent.height - color: streamEntry._muted ? M.Theme.base04 : root.accentColor - radius: 2 + function _set(mouse) { + if (!streamEntry.modelData.audio) + return; + streamEntry.modelData.audio.volume = Math.max(0, Math.min(1, mouse.x / streamSlider.width)); } - - MouseArea { - anchors.fill: parent - anchors.margins: -6 - cursorShape: Qt.PointingHandCursor - onPressed: mouse => _set(mouse) - onPositionChanged: mouse => { - if (pressed) - _set(mouse); - } - function _set(mouse) { - if (!streamEntry.modelData.audio) - return; - streamEntry.modelData.audio.volume = Math.max(0, Math.min(1, mouse.x / streamSlider.width)); - } - } - } - - Text { - id: streamVol - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: Math.round(streamEntry._vol * 100) + "%" - color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - width: 28 } } - } - // Bottom padding - Item { - width: 1 - height: 4 + Text { + id: streamVol + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: Math.round(streamEntry._vol * 100) + "%" + color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + width: 28 + } } } + + // Bottom padding + Item { + width: 1 + height: 4 + } } } } diff --git a/modules/qmldir b/modules/qmldir index 62b0404..c79fa03 100644 --- a/modules/qmldir +++ b/modules/qmldir @@ -14,6 +14,7 @@ Tray 1.0 Tray.qml TrayMenu 1.0 TrayMenu.qml PopupPanel 1.0 PopupPanel.qml PopupBackground 1.0 PopupBackground.qml +HoverPanel 1.0 HoverPanel.qml PowerMenu 1.0 PowerMenu.qml ScreenCorners 1.0 ScreenCorners.qml ThemedIcon 1.0 ThemedIcon.qml