Compare commits

...

3 commits

10 changed files with 159 additions and 242 deletions

View file

@ -171,6 +171,7 @@ PanelWindow {
} }
M.Volume { M.Volume {
visible: M.Modules.volume.enable visible: M.Modules.volume.enable
bar: bar
} }
} }

View file

@ -3,10 +3,11 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import "." as M import "." as M
M.PopupPanel { M.HoverPanel {
id: menuWindow id: menuWindow
panelWidth: 250 popupMode: true
contentWidth: 250
property var _devices: [] property var _devices: []

View file

@ -222,7 +222,7 @@ M.BarSection {
anchors.rightMargin: 12 anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: parent._f.toFixed(2) text: parent._f.toFixed(2)
color: parent._barColor color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2 font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily font.family: M.Theme.fontFamily
width: 32 width: 32

View file

@ -3,18 +3,36 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import "." as M import "." as M
// Shared hover/OSD panel PanelWindow slides down from the bar on hover or // Unified bar panel fullscreen transparent window so content can resize
// external trigger. Parent module computes showPanel and reads panelHovered. // freely without triggering Wayland surface resizes.
//
// Hover mode (popupMode: false, default):
// Parent drives visibility via showPanel. Panel auto-closes when showPanel
// drops, with a 50ms debounce. Reports panelHovered back to parent.
// Pass anchorItem for lazy position computation on each show.
//
// Popup mode (popupMode: true):
// Shows immediately on creation. Stays open until click-outside or
// dismiss() call. Emits dismissed() when closed caller's LazyLoader
// sets active: false. Pass anchorX (screen-relative centre x).
PanelWindow { PanelWindow {
id: root id: root
required property bool showPanel property bool popupMode: false
required property Item anchorItem
// Hover mode
property bool showPanel: true
property Item anchorItem: null
property bool panelHovered: false
// Popup mode
property real anchorX: -1
signal dismissed
// Shared
required property color accentColor required property color accentColor
property string panelNamespace: "nova-panel" property string panelNamespace: "nova-panel"
property real contentWidth: 220 property real contentWidth: 220
property bool animateHeight: false
property bool panelHovered: false
default property alias content: panelContent.children default property alias content: panelContent.children
@ -29,49 +47,52 @@ PanelWindow {
anchors.top: true anchors.top: true
anchors.left: true anchors.left: true
anchors.right: true
anchors.bottom: true
margins.top: 0 margins.top: 0
implicitWidth: panelContent.width
implicitHeight: panelContent.height
Behavior on implicitHeight {
enabled: root.animateHeight
NumberAnimation {
duration: 100
easing.type: Easing.OutCubic
}
}
function _updatePosition() { function _updatePosition() {
const pt = anchorItem.mapToGlobal(anchorItem.width / 2, 0);
const scr = screen; const scr = screen;
const sw = scr?.width ?? 1920; const sw = scr?.width ?? 1920;
margins.left = Math.max(0, Math.min(Math.round(pt.x - (scr?.x ?? 0) - contentWidth / 2), sw - contentWidth)); let cx;
if (root.anchorItem) {
const pt = root.anchorItem.mapToGlobal(root.anchorItem.width / 2, 0);
cx = pt.x - (scr?.x ?? 0);
} else {
cx = root.anchorX;
}
panelContainer.x = Math.max(0, Math.min(Math.round(cx - root.contentWidth / 2), sw - root.contentWidth));
} }
function _show() {
_updatePosition();
_winVisible = true;
hideAnim.stop();
showAnim.start();
}
function dismiss() {
showAnim.stop();
hideAnim.start();
}
Component.onCompleted: if (popupMode)
_show()
Timer { Timer {
id: _hideTimer id: _hideTimer
interval: 50 interval: 50
onTriggered: { onTriggered: if (!root.showPanel)
if (!root.showPanel) { root.dismiss()
console.log("[hp:" + panelNamespace + "] hideTimer fired, starting hideAnim");
showAnim.stop();
hideAnim.start();
}
}
} }
on_WinVisibleChanged: console.log("[hp:" + panelNamespace + "] _winVisible →", _winVisible)
onShowPanelChanged: { onShowPanelChanged: {
console.log("[hp:" + panelNamespace + "] showPanel →", showPanel); if (root.popupMode)
return;
if (showPanel) { if (showPanel) {
_hideTimer.stop(); _hideTimer.stop();
_updatePosition(); _show();
_winVisible = true;
hideAnim.stop();
showAnim.start();
} else { } else {
_hideTimer.restart(); _hideTimer.restart();
} }
@ -80,14 +101,14 @@ PanelWindow {
ParallelAnimation { ParallelAnimation {
id: showAnim id: showAnim
NumberAnimation { NumberAnimation {
target: panelContent target: panelContainer
property: "opacity" property: "opacity"
to: 1 to: 1
duration: 120 duration: 120
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
} }
NumberAnimation { NumberAnimation {
target: panelContent target: panelContainer
property: "y" property: "y"
to: 0 to: 0
duration: 150 duration: 150
@ -98,57 +119,81 @@ PanelWindow {
ParallelAnimation { ParallelAnimation {
id: hideAnim id: hideAnim
NumberAnimation { NumberAnimation {
target: panelContent target: panelContainer
property: "opacity" property: "opacity"
to: 0 to: 0
duration: 150 duration: 150
easing.type: Easing.InCubic easing.type: Easing.InCubic
} }
NumberAnimation { NumberAnimation {
target: panelContent target: panelContainer
property: "y" property: "y"
to: -panelContent.height to: -panelContainer.height
duration: 150 duration: 150
easing.type: Easing.InCubic easing.type: Easing.InCubic
} }
onStarted: console.log("[hp:" + panelNamespace + "] hideAnim started") onFinished: {
onFinished: root._winVisible = false root._winVisible = false;
} if (root.popupMode)
root.dismissed();
HoverHandler {
onHoveredChanged: {
console.log("[hp:" + panelNamespace + "] hovered →", hovered);
root.panelHovered = hovered;
} }
} }
// Popup mode: click-outside dismiss (declared first = lowest z = below content)
MouseArea {
anchors.fill: parent
visible: root.popupMode
enabled: root.popupMode
onClicked: root.dismiss()
}
M.PopupBackground { M.PopupBackground {
x: panelContent.x x: panelContainer.x
y: panelContent.y y: panelContainer.y
width: panelContent.width width: panelContainer.width
height: panelContent.height height: panelContainer.height
opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85) opacity: panelContainer.opacity * Math.max(M.Theme.barOpacity, 0.85)
accentColor: root.accentColor accentColor: root.accentColor
} }
Column { Item {
id: panelContent id: panelContainer
width: root.contentWidth x: 0
opacity: 0
y: -height y: -height
width: root.contentWidth
height: panelContent.height
opacity: 0
// Popup mode: eat clicks on panel background so outer dismiss doesn't fire
MouseArea {
anchors.fill: parent
visible: root.popupMode
enabled: root.popupMode
}
Column {
id: panelContent
width: root.contentWidth
HoverHandler {
enabled: !root.popupMode
onHoveredChanged: if (!root.popupMode)
root.panelHovered = hovered
}
}
} }
// Border overlay drawn on top of content so full-bleed items (e.g. album art) don't cover it // Border overlay on top of content so full-bleed items don't cover it
Rectangle { Rectangle {
x: panelContent.x x: panelContainer.x
y: panelContent.y y: panelContainer.y
width: panelContent.width width: panelContainer.width
height: panelContent.height height: panelContainer.height
color: "transparent" color: "transparent"
border.color: root.accentColor border.color: root.accentColor
border.width: 1 border.width: 1
bottomLeftRadius: M.Theme.radius bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius bottomRightRadius: M.Theme.radius
opacity: panelContent.opacity opacity: panelContainer.opacity
} }
} }

View file

@ -103,7 +103,6 @@ M.BarSection {
accentColor: root.accentColor accentColor: root.accentColor
panelNamespace: "nova-mpris" panelNamespace: "nova-mpris"
contentWidth: 280 contentWidth: 280
animateHeight: true
// Album art // Album art
Item { Item {

View file

@ -3,10 +3,11 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import "." as M import "." as M
M.PopupPanel { M.HoverPanel {
id: menuWindow id: menuWindow
panelWidth: 250 popupMode: true
contentWidth: 250
property var _networks: [] property var _networks: []

View file

@ -1,111 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "." as M
// Shared flyout popup window slides down from the bar, dismisses on
// click outside. Created on demand via Loader; animates in on creation,
// animates out then emits dismissed() for the Loader to deactivate.
PanelWindow {
id: root
default property alias content: contentCol.children
required property var screen
required property real anchorX
property real panelWidth: 220
property color accentColor: M.Theme.base05
signal dismissed
visible: true
color: "transparent"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: 0
WlrLayershell.namespace: "nova-popup"
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
Component.onCompleted: showAnim.start()
function dismiss() {
showAnim.stop();
hideAnim.start();
}
// Click outside dismiss
MouseArea {
anchors.fill: parent
onClicked: root.dismiss()
}
Item {
id: panel
x: Math.max(0, Math.min(Math.round(root.anchorX - contentCol.width / 2), root.width - contentCol.width))
y: 0
width: contentCol.width
height: contentCol.height
opacity: 0
// Eat clicks inside the panel
MouseArea {
anchors.fill: parent
}
M.PopupBackground {
anchors.fill: parent
accentColor: root.accentColor
}
Column {
id: contentCol
width: root.panelWidth
topPadding: 4
bottomPadding: 4
spacing: 2
}
}
ParallelAnimation {
id: showAnim
NumberAnimation {
target: panel
property: "opacity"
from: 0
to: 1
duration: 150
easing.type: Easing.OutCubic
}
NumberAnimation {
target: panel
property: "y"
from: -panel.height
to: 0
duration: 200
easing.type: Easing.OutCubic
}
}
ParallelAnimation {
id: hideAnim
NumberAnimation {
target: panel
property: "opacity"
to: 0
duration: 150
easing.type: Easing.InCubic
}
NumberAnimation {
target: panel
property: "y"
to: -panel.height
duration: 150
easing.type: Easing.InCubic
}
onFinished: root.dismissed()
}
}

View file

@ -2,10 +2,11 @@ import QtQuick
import Quickshell import Quickshell
import "." as M import "." as M
M.PopupPanel { M.HoverPanel {
id: menuWindow id: menuWindow
panelWidth: 180 popupMode: true
contentWidth: 180
signal runCommand(var cmd) signal runCommand(var cmd)
@ -56,7 +57,7 @@ M.PopupPanel {
required property var modelData required property var modelData
required property int index required property int index
width: menuWindow.panelWidth width: menuWindow.contentWidth
height: 32 height: 32
Rectangle { Rectangle {

View file

@ -38,18 +38,9 @@ M.BarSection {
return streams; return streams;
} }
property bool _expanded: false
property bool _osdActive: false property bool _osdActive: false
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
readonly property bool _showPanel: _anyHover || _expanded || _osdActive readonly property bool _showPanel: _anyHover || _osdActive
on_ShowPanelChanged: {
console.log("[vol] showPanel →", _showPanel, "| expanded:", _expanded, "| anyHover:", _anyHover);
if (!_showPanel)
_expanded = false;
}
on_ExpandedChanged: console.log("[vol] expanded →", _expanded)
onVolumeChanged: _flashPanel() onVolumeChanged: _flashPanel()
onMutedChanged: _flashPanel() onMutedChanged: _flashPanel()
@ -65,6 +56,8 @@ M.BarSection {
onTriggered: root._osdActive = false onTriggered: root._osdActive = false
} }
required property var bar
M.BarIcon { M.BarIcon {
icon: root._volumeIcon icon: root._volumeIcon
minIcon: "\uF028" minIcon: "\uF028"
@ -98,7 +91,7 @@ M.BarSection {
} }
} }
// Unified volume panel hover shows slider, click expands to show devices // OSD panel hover shows slider only, fixed height, no resize
M.HoverPanel { M.HoverPanel {
id: hoverPanel id: hoverPanel
showPanel: root._showPanel showPanel: root._showPanel
@ -107,14 +100,12 @@ M.BarSection {
accentColor: root.accentColor accentColor: root.accentColor
panelNamespace: "nova-volume" panelNamespace: "nova-volume"
contentWidth: 220 contentWidth: 220
animateHeight: true
// Compact: slider row // Slider row
Item { Item {
width: parent.width width: parent.width
height: 36 height: 36
// Mute toggle
Text { Text {
id: muteIcon id: muteIcon
anchors.left: parent.left anchors.left: parent.left
@ -132,7 +123,6 @@ M.BarSection {
} }
} }
// Slider
Item { Item {
id: slider id: slider
anchors.left: muteIcon.right anchors.left: muteIcon.right
@ -147,13 +137,11 @@ M.BarSection {
color: M.Theme.base02 color: M.Theme.base02
radius: 3 radius: 3
} }
Rectangle { Rectangle {
width: parent.width * Math.min(1, Math.max(0, root.volume)) width: parent.width * Math.min(1, Math.max(0, root.volume))
height: parent.height height: parent.height
color: root._volumeColor color: root._volumeColor
radius: 3 radius: 3
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
duration: 80 duration: 80
@ -191,7 +179,7 @@ M.BarSection {
} }
} }
// Sink name click to expand/collapse device list // Sink name row click chevron to open mixer popup
Item { Item {
width: parent.width width: parent.width
height: 22 height: 22
@ -214,7 +202,7 @@ M.BarSection {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: root._expanded ? "\uF077" : "\uF078" text: "\uF078"
color: M.Theme.base04 color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 3 font.pixelSize: M.Theme.fontSize - 3
font.family: M.Theme.iconFontFamily font.family: M.Theme.iconFontFamily
@ -222,17 +210,24 @@ M.BarSection {
TapHandler { TapHandler {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onTapped: root._expanded = true onTapped: mixerLoader.active = true
} }
} }
}
// Expanded: output device list // Mixer popup separate window, no resize issues
Column { LazyLoader {
id: deviceList id: mixerLoader
width: parent.width active: false
visible: root._expanded
// Separator M.PopupPanel {
accentColor: root.accentColor
screen: root.bar.screen
anchorX: root.mapToGlobal(root.width / 2, 0).x - (root.bar.screen?.x ?? 0)
panelWidth: 220
onDismissed: mixerLoader.active = false
// Output devices
Rectangle { Rectangle {
width: parent.width - 16 width: parent.width - 16
height: 1 height: 1
@ -240,7 +235,6 @@ M.BarSection {
color: M.Theme.base03 color: M.Theme.base03
} }
// Header
Text { Text {
width: parent.width width: parent.width
height: 24 height: 24
@ -258,7 +252,7 @@ M.BarSection {
delegate: Item { delegate: Item {
required property var modelData required property var modelData
width: deviceList.width width: 220
height: 28 height: 28
readonly property bool _active: modelData === root.sink readonly property bool _active: modelData === root.sink
@ -293,38 +287,31 @@ M.BarSection {
TapHandler { TapHandler {
onTapped: { onTapped: {
Pipewire.preferredDefaultAudioSink = modelData; Pipewire.preferredDefaultAudioSink = modelData;
root._expanded = false; mixerLoader.active = false;
} }
} }
} }
} }
// Streams header (only if there are streams) // Streams section
Item { Rectangle {
visible: root._streamList.length > 0
width: parent.width - 16
height: visible ? 1 : 0
anchors.horizontalCenter: parent.horizontalCenter
color: M.Theme.base03
}
Text {
visible: root._streamList.length > 0 visible: root._streamList.length > 0
width: parent.width width: parent.width
height: visible ? streamSep.height + streamHeader.height : 0 height: visible ? 24 : 0
verticalAlignment: Text.AlignVCenter
Rectangle { leftPadding: 12
id: streamSep text: "Applications"
width: parent.width - 16 color: M.Theme.base04
height: 1 font.pixelSize: M.Theme.fontSize - 1
anchors.horizontalCenter: parent.horizontalCenter font.family: M.Theme.fontFamily
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 { Repeater {
@ -334,7 +321,7 @@ M.BarSection {
id: streamEntry id: streamEntry
required property var modelData required property var modelData
width: deviceList.width width: 220
height: 32 height: 32
readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown" readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown"
@ -422,12 +409,6 @@ M.BarSection {
} }
} }
} }
// Bottom padding
Item {
width: 1
height: 4
}
} }
} }
} }

View file

@ -12,7 +12,6 @@ Clock 1.0 Clock.qml
Volume 1.0 Volume.qml Volume 1.0 Volume.qml
Tray 1.0 Tray.qml Tray 1.0 Tray.qml
TrayMenu 1.0 TrayMenu.qml TrayMenu 1.0 TrayMenu.qml
PopupPanel 1.0 PopupPanel.qml
PopupBackground 1.0 PopupBackground.qml PopupBackground 1.0 PopupBackground.qml
HoverPanel 1.0 HoverPanel.qml HoverPanel 1.0 HoverPanel.qml
PowerMenu 1.0 PowerMenu.qml PowerMenu 1.0 PowerMenu.qml