diff --git a/modules/Backlight.qml b/modules/Backlight.qml index f98a862..354db87 100644 --- a/modules/Backlight.qml +++ b/modules/Backlight.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Io +import Quickshell.Wayland import "." as M M.BarSection { @@ -11,8 +12,9 @@ M.BarSection { tooltip: "" property int percent: 0 + property bool _panelHovered: false property bool _osdActive: false - readonly property bool _showPanel: root._hovered || hoverPanel.panelHovered || _osdActive + readonly property bool _showPanel: root._hovered || _panelHovered || _osdActive onPercentChanged: if (percent > 0) _flashPanel() @@ -95,83 +97,174 @@ M.BarSection { onWheel: event => root.adjust(event.angleDelta.y) } - 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 + PanelWindow { + id: panel - Item { - width: parent.width - height: 36 + screen: QsWindow.window?.screen ?? null + visible: _winVisible + color: "transparent" - 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 + 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(); + } } + } + + 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 + } + + Rectangle { + x: panelContent.x + y: panelContent.y + width: panelContent.width + height: panelContent.height + color: M.Theme.base01 + opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85) + topLeftRadius: 0 + topRightRadius: 0 + bottomLeftRadius: M.Theme.radius + bottomRightRadius: M.Theme.radius + border.color: root.accentColor + border.width: 1 + } + + Column { + id: panelContent + width: 200 + opacity: 0 + y: -height Item { - id: slider - anchors.left: blIcon.right - anchors.leftMargin: 8 - anchors.right: blLabel.left - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - height: 6 + width: parent.width + height: 36 - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 3 + 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 } - Rectangle { - width: parent.width * root.percent / 100 - height: parent.height - color: root.accentColor - 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 - Behavior on width { - NumberAnimation { - duration: 80 + 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); } } } - 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 } } - - 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/Bluetooth.qml b/modules/Bluetooth.qml index 68181dc..6dd46d6 100644 --- a/modules/Bluetooth.qml +++ b/modules/Bluetooth.qml @@ -101,10 +101,10 @@ M.BarSection { } } - LazyLoader { + Loader { id: menuLoader active: false - M.BluetoothMenu { + sourceComponent: M.BluetoothMenu { accentColor: root.accentColor screen: root.bar.screen anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) diff --git a/modules/Flyout.qml b/modules/Flyout.qml index b2e7e03..58539c0 100644 --- a/modules/Flyout.qml +++ b/modules/Flyout.qml @@ -83,9 +83,16 @@ PanelWindow { opacity: 0 y: -height - M.PopupBackground { + Rectangle { anchors.fill: parent - accentColor: M.FlyoutState.accentColor + color: M.Theme.base01 + opacity: Math.max(M.Theme.barOpacity, 0.85) + topLeftRadius: 0 + topRightRadius: 0 + bottomLeftRadius: M.Theme.radius + bottomRightRadius: M.Theme.radius + border.color: M.FlyoutState.accentColor + border.width: 1 } Text { diff --git a/modules/HoverPanel.qml b/modules/HoverPanel.qml deleted file mode 100644 index f7cb7a2..0000000 --- a/modules/HoverPanel.qml +++ /dev/null @@ -1,115 +0,0 @@ -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 fe15f2c..0258397 100644 --- a/modules/Mpris.qml +++ b/modules/Mpris.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Io +import Quickshell.Wayland import Quickshell.Services.Mpris import "." as M @@ -57,8 +58,9 @@ M.BarSection { required property var bar + property bool _panelHovered: false property bool _pinned: false - readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered + readonly property bool _anyHover: root._hovered || _panelHovered readonly property bool _showPanel: _anyHover || _pinned on_AnyHoverChanged: { @@ -95,290 +97,389 @@ M.BarSection { } } - 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 + PanelWindow { + id: panel - // Album art - Item { - width: parent.width - height: _artImg._hasArt ? 140 : 60 - clip: true + screen: QsWindow.window?.screen ?? null + visible: _winVisible + color: "transparent" - Rectangle { - anchors.fill: parent - color: M.Theme.base02 + 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 + } + } + + Connections { + target: root + function on_ShowPanelChanged() { + if (root._showPanel) { + panel._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: panel._winVisible = false + } + + HoverHandler { + onHoveredChanged: root._panelHovered = hovered + } + + Rectangle { + x: panelContent.x + y: panelContent.y + width: panelContent.width + height: panelContent.height + color: M.Theme.base01 + opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85) + topLeftRadius: 0 + topRightRadius: 0 + bottomLeftRadius: M.Theme.radius + bottomRightRadius: M.Theme.radius + border.color: root.accentColor + border.width: 1 + } + + 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; + } + } + } + + // 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 + + 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 + } } - Image { - id: _artImg - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - visible: _hasArt - asynchronous: true - source: root._cachedArt + // Track info + Item { + width: parent.width + height: titleCol.implicitHeight + 8 - property bool _hasArt: false - onStatusChanged: if (status === Image.Ready) - _hasArt = true - Connections { - target: root - function on_CachedArtChanged() { - if (!root._cachedArt) - _artImg._hasArt = false; + 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 !== "" } } } - // 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 + // 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 - 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 + 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: 1 + radius: 2 + } + } + } - Behavior on height { - NumberAnimation { - duration: 50 - } + // 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() } } } } - 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.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 - + // Player switcher Row { - anchors.centerIn: parent - spacing: 24 + anchors.horizontalCenter: parent.horizontalCenter + height: 22 + spacing: 6 - 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() - } - } + Repeater { + model: root._players - 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() - } - } + delegate: Rectangle { + required property var modelData + required property int index - 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() - } - } - } - } + readonly property bool _active: index === root._playerIdx - // Player switcher - Row { - anchors.horizontalCenter: parent.horizontalCenter - height: 22 - spacing: 6 + 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 - Repeater { - model: root._players + 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 + } - 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 + MouseArea { + id: pArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root._playerIdx = index + } } } } diff --git a/modules/Network.qml b/modules/Network.qml index 9c1b7b5..01677c5 100644 --- a/modules/Network.qml +++ b/modules/Network.qml @@ -121,10 +121,10 @@ M.BarSection { } } - LazyLoader { + Loader { id: menuLoader active: false - M.NetworkMenu { + sourceComponent: M.NetworkMenu { accentColor: root.accentColor screen: root.bar.screen anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) diff --git a/modules/Notifications.qml b/modules/Notifications.qml index c4abc02..6b7c9ea 100644 --- a/modules/Notifications.qml +++ b/modules/Notifications.qml @@ -77,10 +77,10 @@ M.BarSection { onTapped: M.NotifService.toggleDnd() } - LazyLoader { + Loader { id: centerLoader active: false - M.NotifCenter { + sourceComponent: M.NotifCenter { accentColor: root.accentColor screen: root.bar.screen anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) diff --git a/modules/PopupBackground.qml b/modules/PopupBackground.qml deleted file mode 100644 index b71da4d..0000000 --- a/modules/PopupBackground.qml +++ /dev/null @@ -1,15 +0,0 @@ -import QtQuick -import "." as M - -Rectangle { - property color accentColor: M.Theme.base05 - - color: M.Theme.base01 - opacity: Math.max(M.Theme.barOpacity, 0.85) - topLeftRadius: 0 - topRightRadius: 0 - bottomLeftRadius: M.Theme.radius - bottomRightRadius: M.Theme.radius - border.color: accentColor - border.width: 1 -} diff --git a/modules/PopupPanel.qml b/modules/PopupPanel.qml index ed8b5e6..706d30a 100644 --- a/modules/PopupPanel.qml +++ b/modules/PopupPanel.qml @@ -56,9 +56,16 @@ PanelWindow { anchors.fill: parent } - M.PopupBackground { + Rectangle { anchors.fill: parent - accentColor: root.accentColor + color: M.Theme.base01 + opacity: Math.max(M.Theme.barOpacity, 0.85) + topLeftRadius: 0 + topRightRadius: 0 + bottomLeftRadius: M.Theme.radius + bottomRightRadius: M.Theme.radius + border.color: root.accentColor + border.width: 1 } Column { diff --git a/modules/Power.qml b/modules/Power.qml index d94335e..0436874 100644 --- a/modules/Power.qml +++ b/modules/Power.qml @@ -23,11 +23,11 @@ M.BarIcon { } } - LazyLoader { + Loader { id: menuLoader active: false - M.PowerMenu { - accentColor: root.accentColor + sourceComponent: M.PowerMenu { + accentColor: parent?.accentColor ?? root.color screen: root.bar.screen anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) onDismissed: menuLoader.active = false diff --git a/modules/Tray.qml b/modules/Tray.qml index 317378d..b8c35d7 100644 --- a/modules/Tray.qml +++ b/modules/Tray.qml @@ -105,10 +105,10 @@ RowLayout { } // Per-icon context menu window, created on demand - LazyLoader { + Loader { id: menuLoader active: false - M.TrayMenu { + sourceComponent: M.TrayMenu { accentColor: root.parent?.accentColor ?? M.Theme.base05 handle: iconItem.modelData.menu screen: root.bar.screen diff --git a/modules/Volume.qml b/modules/Volume.qml index 8f8bea5..134a54d 100644 --- a/modules/Volume.qml +++ b/modules/Volume.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Wayland import Quickshell.Services.Pipewire import "." as M @@ -39,13 +40,11 @@ M.BarSection { } property bool _expanded: false + property bool _panelHovered: false property bool _osdActive: false - readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered + readonly property bool _anyHover: root._hovered || _panelHovered readonly property bool _showPanel: _anyHover || _expanded || _osdActive - on_ShowPanelChanged: if (!_showPanel) - _expanded = false - onVolumeChanged: _flashPanel() onMutedChanged: _flashPanel() @@ -105,319 +104,414 @@ M.BarSection { } // Unified volume panel — hover shows slider, click expands to show devices - 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 + PanelWindow { + id: panel - // Compact: slider row - Item { - width: parent.width - height: 36 + screen: QsWindow.window?.screen ?? null + visible: _winVisible + color: "transparent" - // 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 + property bool _winVisible: false - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: if (root.sink?.audio) - root.sink.audio.muted = !root.sink.audio.muted + 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(); } } + } - // Slider + 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 + } + + Rectangle { + x: panelContent.x + y: panelContent.y + width: panelContent.width + height: panelContent.height + color: M.Theme.base01 + opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85) + topLeftRadius: 0 + topRightRadius: 0 + bottomLeftRadius: M.Theme.radius + bottomRightRadius: M.Theme.radius + border.color: root.accentColor + border.width: 1 + } + + Column { + id: panelContent + width: 220 + opacity: 0 + y: -height + + // Compact: slider row Item { - id: slider - anchors.left: muteIcon.right - anchors.leftMargin: 8 - anchors.right: volLabel.left - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - height: 6 + width: parent.width + height: 36 - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 3 + // 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 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: if (root.sink?.audio) + root.sink.audio.muted = !root.sink.audio.muted + } } - Rectangle { - width: parent.width * Math.min(1, Math.max(0, root.volume)) - height: parent.height - color: root._volumeColor - radius: 3 + // Slider + Item { + id: slider + anchors.left: muteIcon.right + anchors.leftMargin: 8 + anchors.right: volLabel.left + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + height: 6 - Behavior on width { - NumberAnimation { - duration: 80 + 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)); } } } - 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)); - } + 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 } } - 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 + // Sink name Text { width: parent.width - height: 24 - verticalAlignment: Text.AlignVCenter - leftPadding: 12 - text: "Output Devices" + height: 18 + horizontalAlignment: Text.AlignHCenter + text: root.sink?.description ?? root.sink?.name ?? "" color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 1 + font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily + elide: Text.ElideRight + leftPadding: 12 + rightPadding: 12 } - Repeater { - model: root._sinkList + // Expanded: output device list + Column { + id: deviceList + width: parent.width + visible: root._expanded + clip: true - 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 + property real _targetHeight: root._expanded ? implicitHeight : 0 + height: _targetHeight + Behavior on height { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic } } - } - - // 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: "Applications" + text: "Output Devices" color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 1 font.family: M.Theme.fontFamily } - } - Repeater { - model: root._streamList + Repeater { + model: root._sinkList - delegate: Item { - id: streamEntry - required property var modelData + delegate: Item { + required property var modelData - width: deviceList.width - height: 32 + width: deviceList.width + height: 28 - 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 - } - } - - 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 + readonly property bool _active: modelData === root.sink Rectangle { anchors.fill: parent - color: M.Theme.base02 - radius: 2 + anchors.leftMargin: 4 + anchors.rightMargin: 4 + color: deviceArea.containsMouse ? M.Theme.base02 : "transparent" + radius: M.Theme.radius } - 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 { + 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 - anchors.margins: -6 + hoverEnabled: true 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)); - } + onClicked: Pipewire.preferredDefaultAudioSink = modelData } } - - 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 + // 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 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + } + } + + Repeater { + model: root._streamList + + 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 + } + } + + 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 + } + 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 + } + + 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 + } } } } diff --git a/modules/qmldir b/modules/qmldir index c79fa03..848ae10 100644 --- a/modules/qmldir +++ b/modules/qmldir @@ -13,8 +13,6 @@ Volume 1.0 Volume.qml 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