refactor: unified BarModule base component, click-to-open panels, remove pinning

This commit is contained in:
Damocles 2026-04-25 11:52:20 +02:00
parent 034f0b6d85
commit 26476dc930
33 changed files with 273 additions and 517 deletions

View file

@ -1,19 +1,16 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import "." as M
import "../services" as S import "../services" as S
M.HoverPanel { Column {
id: menuWindow id: root
popupMode: true
contentWidth: 180
required property color accentColor
signal runCommand(var cmd) signal runCommand(var cmd)
signal dismiss
readonly property bool _isNiri: Quickshell.env("NIRI_SOCKET") !== "" readonly property bool _isNiri: Quickshell.env("NIRI_SOCKET") !== ""
// Confirmation state: null = normal menu, object = pending confirm
property var _confirmItem: null property var _confirmItem: null
function _run(cmd) { function _run(cmd) {
@ -35,8 +32,8 @@ M.HoverPanel {
// Normal menu entries // Normal menu entries
Column { Column {
visible: !menuWindow._confirmItem visible: !root._confirmItem
width: menuWindow.contentWidth width: root.width
Repeater { Repeater {
model: [ model: [
@ -57,7 +54,7 @@ M.HoverPanel {
{ {
label: "Logout", label: "Logout",
icon: "\uF2F5", icon: "\uF2F5",
cmd: menuWindow._isNiri ? ["niri", "msg", "action", "quit"] : ["loginctl", "terminate-user", ""], cmd: root._isNiri ? ["niri", "msg", "action", "quit"] : ["loginctl", "terminate-user", ""],
color: S.Theme.base0A, color: S.Theme.base0A,
confirm: false confirm: false
}, },
@ -83,7 +80,7 @@ M.HoverPanel {
required property var modelData required property var modelData
required property int index required property int index
width: menuWindow.contentWidth width: root.width
height: 32 height: 32
Rectangle { Rectangle {
@ -121,7 +118,7 @@ M.HoverPanel {
} }
TapHandler { TapHandler {
onTapped: menuWindow._requestAction(entry.modelData) onTapped: root._requestAction(entry.modelData)
} }
} }
} }
@ -129,8 +126,8 @@ M.HoverPanel {
// Confirmation view // Confirmation view
Column { Column {
visible: !!menuWindow._confirmItem visible: !!root._confirmItem
width: menuWindow.contentWidth width: root.width
spacing: 4 spacing: 4
Item { Item {
@ -139,8 +136,8 @@ M.HoverPanel {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: menuWindow._confirmItem ? menuWindow._confirmItem.label + "?" : "" text: root._confirmItem ? root._confirmItem.label + "?" : ""
color: menuWindow._confirmItem ? menuWindow._confirmItem.color : S.Theme.base05 color: root._confirmItem ? root._confirmItem.color : S.Theme.base05
font.pixelSize: S.Theme.fontSize font.pixelSize: S.Theme.fontSize
font.family: S.Theme.fontFamily font.family: S.Theme.fontFamily
font.bold: true font.bold: true
@ -151,7 +148,6 @@ M.HoverPanel {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: 8 spacing: 8
// Cancel
Item { Item {
width: 72 width: 72
height: 28 height: 28
@ -178,11 +174,10 @@ M.HoverPanel {
} }
TapHandler { TapHandler {
onTapped: menuWindow._cancelConfirm() onTapped: root._cancelConfirm()
} }
} }
// Confirm
Item { Item {
width: 72 width: 72
height: 28 height: 28
@ -190,20 +185,20 @@ M.HoverPanel {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: { color: {
if (!menuWindow._confirmItem) if (!root._confirmItem)
return S.Theme.base02; return S.Theme.base02;
const c = menuWindow._confirmItem.color; const c = root._confirmItem.color;
return confirmHover.hovered ? Qt.rgba(c.r, c.g, c.b, 0.3) : Qt.rgba(c.r, c.g, c.b, 0.15); return confirmHover.hovered ? Qt.rgba(c.r, c.g, c.b, 0.3) : Qt.rgba(c.r, c.g, c.b, 0.15);
} }
radius: S.Theme.radius radius: S.Theme.radius
border.width: 1 border.width: 1
border.color: menuWindow._confirmItem ? menuWindow._confirmItem.color : S.Theme.base03 border.color: root._confirmItem ? root._confirmItem.color : S.Theme.base03
} }
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "Confirm" text: "Confirm"
color: menuWindow._confirmItem ? menuWindow._confirmItem.color : S.Theme.base05 color: root._confirmItem ? root._confirmItem.color : S.Theme.base05
font.pixelSize: S.Theme.fontSize font.pixelSize: S.Theme.fontSize
font.family: S.Theme.fontFamily font.family: S.Theme.fontFamily
font.bold: true font.bold: true
@ -216,8 +211,8 @@ M.HoverPanel {
TapHandler { TapHandler {
onTapped: { onTapped: {
if (menuWindow._confirmItem) if (root._confirmItem)
menuWindow._run(menuWindow._confirmItem.cmd); root._run(root._confirmItem.cmd);
} }
} }
} }

View file

@ -13,6 +13,7 @@ MemoryApplet 1.0 MemoryApplet.qml
MprisApplet 1.0 MprisApplet.qml MprisApplet 1.0 MprisApplet.qml
NetworkApplet 1.0 NetworkApplet.qml NetworkApplet 1.0 NetworkApplet.qml
NotifApplet 1.0 NotifApplet.qml NotifApplet 1.0 NotifApplet.qml
PowerApplet 1.0 PowerApplet.qml
Separator 1.0 Separator.qml Separator 1.0 Separator.qml
SparklineCanvas 1.0 SparklineCanvas.qml SparklineCanvas 1.0 SparklineCanvas.qml
TemperatureApplet 1.0 TemperatureApplet.qml TemperatureApplet 1.0 TemperatureApplet.qml

View file

@ -4,12 +4,12 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.OsdSection { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
opacity: S.Modules.backlight.enable && S.BacklightService.available ? 1 : 0 opacity: S.Modules.backlight.enable && S.BacklightService.available ? 1 : 0
visible: opacity > 0 visible: opacity > 0
_panelHovered: hoverPanel.panelHovered tooltip: "Brightness: " + percent + "%"
property int percent: S.BacklightService.percent property int percent: S.BacklightService.percent
property bool _percentInit: false property bool _percentInit: false
@ -27,24 +27,6 @@ M.OsdSection {
onWheel: event => S.BacklightService.adjust(event.angleDelta.y) onWheel: event => S.BacklightService.adjust(event.angleDelta.y)
} }
M.HoverPanel {
id: hoverPanel
showPanel: root._showPanel
screen: QsWindow.window?.screen ?? null
anchorItem: root
accentColor: root.accentColor
panelNamespace: "nova-backlight"
panelTitle: "Brightness"
contentWidth: 200
C.BacklightApplet {
width: parent.width
percent: root.percent
accentColor: root.accentColor
onSetPercent: pct => S.BacklightService.setPercent(pct)
}
}
M.BarIcon { M.BarIcon {
icon: "\uF185" icon: "\uF185"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -54,4 +36,23 @@ M.OsdSection {
minText: "100%" minText: "100%"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
M.HoverPanel {
id: hoverPanel
showPanel: root._showPanel
screen: QsWindow.window?.screen ?? null
anchorItem: root
accentColor: root.accentColor
panelNamespace: "nova-backlight"
panelTitle: "Brightness"
contentWidth: 200
onDismissed: root.dismissPanel()
C.BacklightApplet {
width: parent.width
percent: root.percent
accentColor: root.accentColor
onSetPercent: pct => S.BacklightService.setPercent(pct)
}
}
} }

View file

@ -221,7 +221,6 @@ PanelWindow {
rightEdge: true rightEdge: true
M.BatteryModule {} M.BatteryModule {}
M.PowerModule { M.PowerModule {
bar: bar
visible: S.Modules.power.enable visible: S.Modules.power.enable
} }
} }

View file

@ -1,15 +1,13 @@
import QtQuick import QtQuick
import Quickshell
import "." as M
import "../services" as S import "../services" as S
// Icon element with crossfade animation on icon change.
// Pure visual component - tooltip handling lives in the parent BarModule.
Text { Text {
id: root id: root
property string icon: "" property string icon: ""
property string tooltip: ""
property string minIcon: "" property string minIcon: ""
property color accentColor: parent?.accentColor ?? S.Theme.base05 property color accentColor: parent?.accentColor ?? S.Theme.base05
property bool _hovered: false
property string _displayIcon: icon property string _displayIcon: icon
property string _pendingIcon: "" property string _pendingIcon: ""
@ -54,23 +52,4 @@ Text {
font.pixelSize: root.font.pixelSize font.pixelSize: root.font.pixelSize
font.family: root.font.family font.family: root.font.family
} }
HoverHandler {
cursorShape: Qt.PointingHandCursor
onHoveredChanged: {
root._hovered = hovered;
if (hovered && root.tooltip !== "") {
M.TooltipState.text = root.tooltip;
M.TooltipState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
M.TooltipState.screen = QsWindow.window?.screen ?? null;
M.TooltipState.accentColor = root.accentColor;
M.TooltipState.visible = true;
} else if (!hovered && root.tooltip !== "") {
M.TooltipState.visible = false;
}
}
}
onTooltipChanged: if (_hovered && tooltip !== "")
M.TooltipState.text = tooltip
} }

View file

@ -1,15 +1,13 @@
import QtQuick import QtQuick
import Quickshell
import "." as M
import "../services" as S import "../services" as S
// Label element with minimum-width support via minText.
// Pure visual component - tooltip handling lives in the parent BarModule.
Text { Text {
id: root id: root
property string label: "" property string label: ""
property string tooltip: ""
property string minText: "" property string minText: ""
property color accentColor: parent?.accentColor ?? S.Theme.base05 property color accentColor: parent?.accentColor ?? S.Theme.base05
property bool _hovered: false
text: label text: label
width: minText ? Math.max(implicitWidth, _minMetrics.width) : implicitWidth width: minText ? Math.max(implicitWidth, _minMetrics.width) : implicitWidth
@ -25,23 +23,4 @@ Text {
font.pixelSize: root.font.pixelSize font.pixelSize: root.font.pixelSize
font.family: root.font.family font.family: root.font.family
} }
HoverHandler {
cursorShape: Qt.PointingHandCursor
onHoveredChanged: {
root._hovered = hovered;
if (hovered && root.tooltip !== "") {
M.TooltipState.text = root.tooltip;
M.TooltipState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
M.TooltipState.screen = QsWindow.window?.screen ?? null;
M.TooltipState.accentColor = root.accentColor;
M.TooltipState.visible = true;
} else if (!hovered && root.tooltip !== "") {
M.TooltipState.visible = false;
}
}
}
onTooltipChanged: if (_hovered && tooltip !== "")
M.TooltipState.text = tooltip
} }

View file

@ -0,0 +1,84 @@
import QtQuick
import Quickshell
import "." as M
import "../services" as S
// Unified base component for all bar modules.
// Provides: tooltip on hover, panel state management, OSD flash support.
//
// On tap: toggles _panelOpen and emits tapped(). Modules that want custom tap
// behavior connect onTapped to their action (the toggle still happens).
//
// Modules with a HoverPanel bind:
// M.HoverPanel { showPanel: root._showPanel; onDismissed: root.dismissPanel() }
//
// Modules without a panel need nothing - the toggle is a harmless no-op.
Row {
id: root
property string tooltip: ""
property bool _hovered: false
property color accentColor: parent?.accentColor ?? S.Theme.base05
property int cursorShape: Qt.PointingHandCursor
// Panel state
property bool _panelOpen: false
property bool _osdActive: false
readonly property bool _showPanel: _panelOpen || _osdActive
signal tapped
function flashPanel() {
_osdActive = true;
_osdTimer.restart();
}
function dismissPanel() {
_panelOpen = false;
_osdActive = false;
_osdTimer.stop();
}
Timer {
id: _osdTimer
interval: 1500
onTriggered: if (!root._panelOpen)
root._osdActive = false
}
on_PanelOpenChanged: {
if (_panelOpen)
M.TooltipState.visible = false;
}
HoverHandler {
cursorShape: root.cursorShape
onHoveredChanged: {
root._hovered = hovered;
if (hovered && root.tooltip !== "" && !root._panelOpen) {
M.TooltipState.text = root.tooltip;
M.TooltipState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
M.TooltipState.screen = QsWindow.window?.screen ?? null;
M.TooltipState.accentColor = root.accentColor;
M.TooltipState.visible = true;
} else if (!hovered && root.tooltip !== "") {
M.TooltipState.visible = false;
}
}
}
TapHandler {
onTapped: {
root._panelOpen = !root._panelOpen;
root.tapped();
}
}
onTooltipChanged: if (_hovered && tooltip !== "" && !_panelOpen)
M.TooltipState.text = tooltip
Behavior on opacity {
NumberAnimation {
duration: 150
}
}
}

View file

@ -1,36 +0,0 @@
import QtQuick
import Quickshell
import "." as M
import "../services" as S
Row {
id: root
property string tooltip: ""
property bool _hovered: false
property color accentColor: parent?.accentColor ?? S.Theme.base05
Behavior on opacity {
NumberAnimation {
duration: 150
}
}
HoverHandler {
cursorShape: Qt.PointingHandCursor
onHoveredChanged: {
root._hovered = hovered;
if (hovered && root.tooltip !== "") {
M.TooltipState.text = root.tooltip;
M.TooltipState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
M.TooltipState.screen = QsWindow.window?.screen ?? null;
M.TooltipState.accentColor = root.accentColor;
M.TooltipState.visible = true;
} else if (!hovered && root.tooltip !== "") {
M.TooltipState.visible = false;
}
}
}
onTooltipChanged: if (_hovered && tooltip !== "")
M.TooltipState.text = tooltip
}

View file

@ -4,12 +4,12 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.PinnableSection { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
opacity: S.Modules.battery.enable && S.BatteryService.available ? 1 : 0 opacity: S.Modules.battery.enable && S.BatteryService.available ? 1 : 0
visible: opacity > 0 visible: opacity > 0
_panelHovered: hoverPanel.panelHovered tooltip: "Battery: " + Math.round(S.BatteryService.percent) + "%" + (S.BatteryService.charging ? " (charging)" : "")
property real _blinkOpacity: 1 property real _blinkOpacity: 1
@ -18,7 +18,6 @@ M.PinnableSection {
minOpacity: 0.45 minOpacity: 0.45
} }
// Bar widgets
M.BarIcon { M.BarIcon {
icon: { icon: {
if (S.BatteryService.charging) if (S.BatteryService.charging)
@ -30,9 +29,6 @@ M.PinnableSection {
opacity: root._blinkOpacity opacity: root._blinkOpacity
font.pixelSize: S.Theme.fontSize + 2 font.pixelSize: S.Theme.fontSize + 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
label: Math.round(S.BatteryService.percent) + "%" label: Math.round(S.BatteryService.percent) + "%"
@ -40,12 +36,8 @@ M.PinnableSection {
color: S.BatteryService.stateColor color: S.BatteryService.stateColor
opacity: root._blinkOpacity opacity: root._blinkOpacity
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
// Hover panel
M.HoverPanel { M.HoverPanel {
id: hoverPanel id: hoverPanel
showPanel: root._showPanel showPanel: root._showPanel
@ -55,6 +47,7 @@ M.PinnableSection {
panelNamespace: "nova-battery" panelNamespace: "nova-battery"
panelTitle: "Battery" panelTitle: "Battery"
contentWidth: 240 contentWidth: 240
onDismissed: root.dismissPanel()
C.BatteryApplet { C.BatteryApplet {
width: hoverPanel.contentWidth width: hoverPanel.contentWidth

View file

@ -4,28 +4,28 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.PinnableSection { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
opacity: S.Modules.bluetooth.enable && S.BluetoothService.state !== "unavailable" ? 1 : 0 opacity: S.Modules.bluetooth.enable && S.BluetoothService.state !== "unavailable" ? 1 : 0
visible: opacity > 0 visible: opacity > 0
_panelHovered: hoverPanel.panelHovered tooltip: {
if (S.BluetoothService.state === "connected")
return "Bluetooth: " + S.BluetoothService.device;
if (S.BluetoothService.state === "off")
return "Bluetooth: off";
return "Bluetooth: on";
}
M.BarIcon { M.BarIcon {
icon: "\uF294" icon: "\uF294"
color: S.BluetoothService.state === "off" ? S.Theme.base04 : root.accentColor color: S.BluetoothService.state === "off" ? S.Theme.base04 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
visible: S.BluetoothService.state === "connected" visible: S.BluetoothService.state === "connected"
label: S.BluetoothService.device + (S.BluetoothService.batteryPct >= 0 ? " " + S.BluetoothService.batteryPct + "%" : "") label: S.BluetoothService.device + (S.BluetoothService.batteryPct >= 0 ? " " + S.BluetoothService.batteryPct + "%" : "")
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
Connections { Connections {
@ -44,6 +44,7 @@ M.PinnableSection {
panelNamespace: "nova-bluetooth" panelNamespace: "nova-bluetooth"
panelTitle: "Bluetooth" panelTitle: "Bluetooth"
contentWidth: 250 contentWidth: 250
onDismissed: root.dismissPanel()
titleActionsComponent: Component { titleActionsComponent: Component {
Item { Item {
width: 20 width: 20

View file

@ -4,10 +4,10 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.PinnableSection { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
_panelHovered: hoverPanel.panelHovered tooltip: Qt.formatDateTime(clock.date, "dddd, dd. MMMM yyyy")
SystemClock { SystemClock {
id: clock id: clock
@ -19,9 +19,6 @@ M.PinnableSection {
label: Qt.formatDateTime(clock.date, "ddd, dd. MMM HH:mm") label: Qt.formatDateTime(clock.date, "ddd, dd. MMM HH:mm")
minText: "Wed, 00. Sep 00:00" minText: "Wed, 00. Sep 00:00"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.HoverPanel { M.HoverPanel {
@ -33,6 +30,7 @@ M.PinnableSection {
panelNamespace: "nova-clock" panelNamespace: "nova-clock"
panelTitle: Qt.formatTime(clock.date, "HH:mm:ss") panelTitle: Qt.formatTime(clock.date, "HH:mm:ss")
contentWidth: 220 contentWidth: 220
onDismissed: root.dismissPanel()
C.ClockApplet { C.ClockApplet {
width: hoverPanel.contentWidth width: hoverPanel.contentWidth

View file

@ -4,10 +4,10 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.PinnableSection { M.BarModule {
id: root id: root
spacing: Math.max(1, S.Theme.moduleSpacing - 2) spacing: Math.max(1, S.Theme.moduleSpacing - 2)
_panelHovered: hoverPanel.panelHovered tooltip: "CPU: " + S.SystemStats.cpuUsage + "% @ " + S.SystemStats.cpuFreqGhz.toFixed(2) + " GHz"
readonly property var _cores: S.SystemStats.cpuCores readonly property var _cores: S.SystemStats.cpuCores
readonly property var _coreMaxFreq: S.SystemStats.cpuCoreMaxFreq readonly property var _coreMaxFreq: S.SystemStats.cpuCoreMaxFreq
@ -34,17 +34,11 @@ M.PinnableSection {
M.BarIcon { M.BarIcon {
icon: "\uF2DB" icon: "\uF2DB"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
label: S.SystemStats.cpuUsage.toString().padStart(2) + "%@" + S.SystemStats.cpuFreqGhz.toFixed(2) label: S.SystemStats.cpuUsage.toString().padStart(2) + "%@" + S.SystemStats.cpuFreqGhz.toFixed(2)
minText: "99%@9.99" minText: "99%@9.99"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.HoverPanel { M.HoverPanel {
@ -56,6 +50,7 @@ M.PinnableSection {
panelNamespace: "nova-cpu" panelNamespace: "nova-cpu"
panelTitle: "CPU" panelTitle: "CPU"
contentWidth: 260 contentWidth: 260
onDismissed: root.dismissPanel()
C.CpuApplet { C.CpuApplet {
width: hoverPanel.contentWidth width: hoverPanel.contentWidth

View file

@ -4,10 +4,10 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.PinnableSection { M.BarModule {
id: root id: root
spacing: Math.max(1, S.Theme.moduleSpacing - 2) spacing: Math.max(1, S.Theme.moduleSpacing - 2)
_panelHovered: hoverPanel.panelHovered tooltip: "Disk: " + _rootPct + "% used"
property var _mounts: S.SystemStats.diskMounts property var _mounts: S.SystemStats.diskMounts
property int _rootPct: S.SystemStats.diskRootPct property int _rootPct: S.SystemStats.diskRootPct
@ -23,18 +23,12 @@ M.PinnableSection {
icon: "\uF0C9" icon: "\uF0C9"
color: root._anyWarn ? S.Theme.base09 : root.accentColor color: root._anyWarn ? S.Theme.base09 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
label: root._rootPct + "%" label: root._rootPct + "%"
minText: "100%" minText: "100%"
color: root._anyWarn ? S.Theme.base09 : root.accentColor color: root._anyWarn ? S.Theme.base09 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.HoverPanel { M.HoverPanel {
@ -46,6 +40,7 @@ M.PinnableSection {
panelNamespace: "nova-disk" panelNamespace: "nova-disk"
panelTitle: "Disk" panelTitle: "Disk"
contentWidth: 260 contentWidth: 260
onDismissed: root.dismissPanel()
C.DiskApplet { C.DiskApplet {
width: hoverPanel.contentWidth width: hoverPanel.contentWidth

View file

@ -4,26 +4,20 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.PinnableSection { M.BarModule {
id: root id: root
spacing: Math.max(1, S.Theme.moduleSpacing - 2) spacing: Math.max(1, S.Theme.moduleSpacing - 2)
visible: S.Modules.gpu.enable && S.SystemStats.gpuAvailable visible: S.Modules.gpu.enable && S.SystemStats.gpuAvailable
_panelHovered: hoverPanel.panelHovered tooltip: "GPU: " + S.SystemStats.gpuUsage + "%"
M.BarIcon { M.BarIcon {
icon: "\uEB4C" icon: "\uEB4C"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
label: S.SystemStats.gpuUsage + "%" label: S.SystemStats.gpuUsage + "%"
minText: "100%" minText: "100%"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.HoverPanel { M.HoverPanel {
@ -35,6 +29,7 @@ M.PinnableSection {
panelNamespace: "nova-gpu" panelNamespace: "nova-gpu"
panelTitle: "GPU" panelTitle: "GPU"
contentWidth: 240 contentWidth: 240
onDismissed: root.dismissPanel()
C.GpuApplet { C.GpuApplet {
width: hoverPanel.contentWidth width: hoverPanel.contentWidth

View file

@ -4,33 +4,19 @@ import Quickshell.Wayland
import "." as M import "." as M
import "../services" as S import "../services" as S
// Unified bar panel fullscreen transparent window so content can resize // Bar panel - fullscreen transparent window so content can resize freely
// freely without triggering Wayland surface resizes. // without triggering Wayland surface resizes.
// //
// Hover mode (popupMode: false, default): // Parent drives visibility via showPanel. Click-outside or Esc dismisses
// Parent drives visibility via showPanel. Panel auto-closes when showPanel // and emits dismissed(). Pass anchorItem for lazy position computation.
// 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
property bool popupMode: false property bool showPanel: false
// Hover mode
property bool showPanel: true
property Item anchorItem: null property Item anchorItem: null
property bool panelHovered: false
// Popup mode
property real anchorX: -1 property real anchorX: -1
signal dismissed signal dismissed
// Shared
required property color accentColor required property color accentColor
property string panelTitle: "" property string panelTitle: ""
property Component titleActionsComponent: null property Component titleActionsComponent: null
@ -43,22 +29,6 @@ PanelWindow {
color: "transparent" color: "transparent"
property bool _winVisible: false property bool _winVisible: false
property bool _pinned: false
property real _dragStartX: 0
property real _dragStartY: 0
property bool _dragging: false
// When pinned: mask = full panel so content is interactive, desktop accessible outside.
// When dragging: mask = null (full screen) so Niri keeps delivering events when cursor
// leaves the panel bounds mid-drag.
mask: (_pinned && !_dragging) ? _pinMask : null
property Region _pinMask: Region {
x: panelContainer.x
y: panelContainer.y
width: panelContainer.width
height: panelContainer.height
}
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: 0 WlrLayershell.exclusiveZone: 0
@ -80,38 +50,29 @@ PanelWindow {
if (root.anchorItem) { if (root.anchorItem) {
const pt = root.anchorItem.mapToGlobal(root.anchorItem.width / 2, 0); const pt = root.anchorItem.mapToGlobal(root.anchorItem.width / 2, 0);
cx = pt.x - (scr?.x ?? 0); cx = pt.x - (scr?.x ?? 0);
} else { } else if (root.anchorX >= 0) {
cx = root.anchorX; cx = root.anchorX;
} else {
cx = sw / 2;
} }
panelContainer.x = Math.max(0, Math.min(Math.round(cx - root.contentWidth / 2), sw - root.contentWidth)); panelContainer.x = Math.max(0, Math.min(Math.round(cx - root.contentWidth / 2), sw - root.contentWidth));
} }
// Grace period: after _show(), suppress auto-close briefly so Niri has time // Grace period: after _show(), suppress click-outside dismiss briefly so
// to route wl_pointer.enter to the new overlay surface (cursor may be stationary). // Niri has time to route wl_pointer.enter to the new overlay surface.
// Popup mode gets a longer window (1500ms) so the user can move the cursor to the
// panel content after clicking the bar without accidentally dismissing it.
property bool _grace: false property bool _grace: false
Timer { Timer {
id: _graceTimer id: _graceTimer
interval: root.popupMode ? 1500 : 400 interval: 400
onTriggered: { onTriggered: root._grace = false
root._grace = false;
if (!root.showPanel && !root._pinned)
root.dismiss();
}
} }
// Content-change grace: call keepOpen(ms) when panel content is about to // Content-change grace: call keepOpen(ms) when panel content is about to
// resize/rebuild (session switch, device list change, etc.) to prevent the // resize/rebuild (session switch, device list change, etc.).
// hover-drop-on-resize from closing the panel.
property bool _contentBusy: false property bool _contentBusy: false
Timer { Timer {
id: _contentBusyTimer id: _contentBusyTimer
onTriggered: { onTriggered: root._contentBusy = false
root._contentBusy = false;
if (!root.showPanel && !root._grace && !root._pinned)
_hideTimer.restart();
}
} }
function keepOpen(ms) { function keepOpen(ms) {
_contentBusy = true; _contentBusy = true;
@ -121,13 +82,7 @@ PanelWindow {
function _show() { function _show() {
_updatePosition(); _updatePosition();
// Only snap to closed position when genuinely opening from scratch.
// If we are interrupting a hide animation, animate back from the current
// y/opacity so there's no visible jump the showAnim NumberAnimations
// always run from the *current* value to their target.
if (!hideAnim.running) { if (!hideAnim.running) {
// Explicitly set y before animating avoids the y:-height binding (live, depends on
// _panelColumn.height) surviving a 00 no-op animation when layout isn't done yet.
panelContainer.y = -(panelContainer.height > 0 ? panelContainer.height : 400); panelContainer.y = -(panelContainer.height > 0 ? panelContainer.height : 400);
panelContainer.opacity = 0; panelContainer.opacity = 0;
} }
@ -144,11 +99,11 @@ PanelWindow {
} }
function dismiss() { function dismiss() {
_pinned = false; if (!_winVisible)
return;
showAnim.stop(); showAnim.stop();
if (S.Theme.reducedMotion) { if (S.Theme.reducedMotion) {
_winVisible = false; _winVisible = false;
if (popupMode)
dismissed(); dismissed();
} else { } else {
hideAnim.start(); hideAnim.start();
@ -157,30 +112,12 @@ PanelWindow {
_graceTimer.stop(); _graceTimer.stop();
} }
Component.onCompleted: if (popupMode)
_show()
Timer {
id: _hideTimer
interval: 150
onTriggered: if (!root.showPanel && !root._grace && !root._pinned && !root._contentBusy)
root.dismiss()
}
onShowPanelChanged: { onShowPanelChanged: {
if (root.popupMode)
return;
if (showPanel) { if (showPanel) {
_hideTimer.stop();
// Only replay the open animation if the panel is actually closed or
// currently animating away. If it is already visible, stopping the
// hide timer is sufficient calling _show() would reset y/opacity to
// 0 and cause a visible flash when the cursor crosses the gap between
// the bar module and the panel.
if (!_winVisible || hideAnim.running) if (!_winVisible || hideAnim.running)
_show(); _show();
} else { } else {
_hideTimer.restart(); dismiss();
} }
} }
@ -220,17 +157,15 @@ PanelWindow {
} }
onFinished: { onFinished: {
root._winVisible = false; root._winVisible = false;
if (root.popupMode)
root.dismissed(); root.dismissed();
} }
} }
// Popup mode: click-outside dismiss. // Click-outside dismiss.
// TapHandler fires for all taps; position check skips taps inside panelContainer. // TapHandler fires for all taps; position check skips taps inside panelContainer.
// Gated on !_grace so spurious events during the 400ms opening window don't dismiss. // Gated on !_grace so spurious events during the opening window don't dismiss.
Item { Item {
anchors.fill: parent anchors.fill: parent
visible: root.popupMode
TapHandler { TapHandler {
enabled: !root._grace enabled: !root._grace
@ -243,6 +178,13 @@ PanelWindow {
} }
} }
// Esc dismiss
Shortcut {
sequence: "Escape"
enabled: root._winVisible
onActivated: root.dismiss()
}
M.PopupBackground { M.PopupBackground {
x: panelContainer.x x: panelContainer.x
y: panelContainer.y y: panelContainer.y
@ -260,52 +202,17 @@ PanelWindow {
height: _panelColumn.height height: _panelColumn.height
opacity: 0 opacity: 0
HoverHandler {
enabled: !root.popupMode && !root._pinned
onHoveredChanged: if (!root.popupMode && !root._pinned)
root.panelHovered = hovered
}
Column { Column {
id: _panelColumn id: _panelColumn
width: root.contentWidth width: root.contentWidth
// Header row: title + action buttons + pin shown in hover mode always, // Header row: title + action buttons
// and in popup mode when a title or actions are provided.
Item { Item {
id: _headerItem id: _headerItem
visible: !root.popupMode || root.panelTitle !== "" || root.titleActionsComponent !== null visible: root.panelTitle !== "" || root.titleActionsComponent !== null
width: parent.width width: parent.width
height: 24 height: 24
// Drag header to freely reposition panel while pinned (hover mode only).
// _dragging clears the input mask so Niri keeps delivering events when the
// cursor leaves the panel bounds during a fast drag.
DragHandler {
enabled: root._pinned && !root.popupMode
onActiveChanged: {
root._dragging = active;
if (active) {
root._dragStartX = panelContainer.x;
root._dragStartY = panelContainer.y;
}
}
onActiveTranslationChanged: {
if (active) {
const sw = root.screen?.width ?? 1920;
const sh = root.screen?.height ?? 1080;
panelContainer.x = Math.max(0, Math.min(root._dragStartX + activeTranslation.x, sw - root.contentWidth));
panelContainer.y = Math.max(0, Math.min(root._dragStartY + activeTranslation.y, sh - panelContainer.height));
}
}
}
// Show move cursor on header when pinned
HoverHandler {
enabled: root._pinned && !root.popupMode
cursorShape: Qt.SizeAllCursor
}
Text { Text {
visible: root.panelTitle !== "" visible: root.panelTitle !== ""
anchors.left: parent.left anchors.left: parent.left
@ -318,52 +225,14 @@ PanelWindow {
font.family: S.Theme.fontFamily font.family: S.Theme.fontFamily
} }
// Action buttons anchored left of pin button slot
Loader { Loader {
id: _titleActionsLoader id: _titleActionsLoader
anchors.right: _pinBtn.left anchors.right: parent.right
anchors.rightMargin: 4 anchors.rightMargin: 4
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
sourceComponent: root.titleActionsComponent sourceComponent: root.titleActionsComponent
} }
// Pin button zero-width in popup mode so actions anchor flush to right
Item {
id: _pinBtn
anchors.right: parent.right
anchors.rightMargin: root.popupMode ? 0 : 4
anchors.verticalCenter: parent.verticalCenter
width: root.popupMode ? 0 : 20
height: 20
visible: !root.popupMode
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
root._pinned = !root._pinned;
if (!root._pinned && !root.showPanel)
root.dismiss();
}
}
Text {
anchors.centerIn: parent
text: root._pinned ? "\uDB81\uDC03" : "\uDB82\uDD31"
color: root._pinned ? root.accentColor : S.Theme.base04
font.pixelSize: S.Theme.fontSize - 1
font.family: S.Theme.iconFontFamily
Behavior on color {
ColorAnimation {
duration: 100
}
}
}
}
// Divider at bottom of header
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@ -391,7 +260,7 @@ PanelWindow {
} }
} }
// Border overlay on top of content so full-bleed items don't cover it // Border overlay - on top of content so full-bleed items don't cover it
Rectangle { Rectangle {
x: panelContainer.x x: panelContainer.x
y: panelContainer.y y: panelContainer.y

View file

@ -3,17 +3,15 @@ import Quickshell
import "." as M import "." as M
import "../services" as S import "../services" as S
M.BarIcon { M.BarModule {
id: root id: root
color: S.IdleInhibitService.active ? S.Theme.base09 : root.accentColor
tooltip: { tooltip: {
const parts = ["Idle inhibition: " + (S.IdleInhibitService.active ? "active" : "inactive")]; const parts = ["Idle inhibition: " + (S.IdleInhibitService.active ? "active" : "inactive")];
if (S.IdleInhibitService.inhibitors) if (S.IdleInhibitService.inhibitors)
parts.push(S.IdleInhibitService.inhibitors); parts.push(S.IdleInhibitService.inhibitors);
return parts.join("\n"); return parts.join("\n");
} }
onTapped: S.IdleInhibitService.toggle()
icon: S.IdleInhibitService.active ? "\uF06E" : "\uF070"
Timer { Timer {
interval: 5000 interval: 5000
@ -23,9 +21,9 @@ M.BarIcon {
onTriggered: S.IdleInhibitService.refreshInhibitors() onTriggered: S.IdleInhibitService.refreshInhibitors()
} }
MouseArea { M.BarIcon {
anchors.fill: parent color: S.IdleInhibitService.active ? S.Theme.base09 : root.accentColor
cursorShape: Qt.PointingHandCursor icon: S.IdleInhibitService.active ? "\uF06E" : "\uF070"
onClicked: S.IdleInhibitService.toggle() anchors.verticalCenter: parent.verticalCenter
} }
} }

View file

@ -4,10 +4,10 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.PinnableSection { M.BarModule {
id: root id: root
spacing: Math.max(1, S.Theme.moduleSpacing - 2) spacing: Math.max(1, S.Theme.moduleSpacing - 2)
_panelHovered: hoverPanel.panelHovered tooltip: "Memory: " + usedGb.toFixed(1) + " / " + totalGb.toFixed(1) + " GB"
property int percent: S.SystemStats.memPercent property int percent: S.SystemStats.memPercent
property real usedGb: S.SystemStats.memUsedGb property real usedGb: S.SystemStats.memUsedGb
@ -25,17 +25,11 @@ M.PinnableSection {
M.BarIcon { M.BarIcon {
icon: "\uEFC5" icon: "\uEFC5"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
label: root.percent + "%" label: root.percent + "%"
minText: "100%" minText: "100%"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.HoverPanel { M.HoverPanel {
@ -47,6 +41,7 @@ M.PinnableSection {
panelNamespace: "nova-memory" panelNamespace: "nova-memory"
panelTitle: "Memory" panelTitle: "Memory"
contentWidth: 240 contentWidth: 240
onDismissed: root.dismissPanel()
C.MemoryApplet { C.MemoryApplet {
width: hoverPanel.contentWidth width: hoverPanel.contentWidth

View file

@ -6,12 +6,12 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.PinnableSection { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
opacity: S.Modules.mpris.enable && player !== null ? 1 : 0 opacity: S.Modules.mpris.enable && player !== null ? 1 : 0
visible: opacity > 0 visible: opacity > 0
_panelHovered: hoverPanel.panelHovered tooltip: player ? (player.trackTitle || player.identity || "Media") + (playing ? " (playing)" : " (paused)") : "Media"
readonly property var _players: S.MprisService.players readonly property var _players: S.MprisService.players
readonly property MprisPlayer player: S.MprisService.player readonly property MprisPlayer player: S.MprisService.player
@ -19,7 +19,6 @@ M.PinnableSection {
property string _cachedArt: "" property string _cachedArt: ""
property string _artTrack: "" property string _artTrack: ""
// Cache art URL at root level so it's captured even when panel is hidden
readonly property string _artUrl: player?.trackArtUrl ?? "" readonly property string _artUrl: player?.trackArtUrl ?? ""
readonly property string _currentTrack: player?.trackTitle ?? "" readonly property string _currentTrack: player?.trackTitle ?? ""
on_ArtUrlChanged: if (_artUrl) on_ArtUrlChanged: if (_artUrl)
@ -74,22 +73,12 @@ M.PinnableSection {
M.BarIcon { M.BarIcon {
icon: root.playing ? "\uF04B" : (root.player?.playbackState === MprisPlaybackState.Paused ? "\uDB80\uDFE4" : "\uDB81\uDCDB") icon: root.playing ? "\uF04B" : (root.player?.playbackState === MprisPlaybackState.Paused ? "\uDB80\uDFE4" : "\uDB81\uDCDB")
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
label: root.player?.trackTitle || root.player?.identity || "" label: root.player?.trackTitle || root.player?.identity || ""
elide: Text.ElideRight elide: Text.ElideRight
width: Math.min(implicitWidth, 200) width: Math.min(implicitWidth, 200)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root._pinned = !root._pinned
}
} }
M.HoverPanel { M.HoverPanel {
@ -101,6 +90,7 @@ M.PinnableSection {
panelNamespace: "nova-mpris" panelNamespace: "nova-mpris"
panelTitle: "Now Playing" panelTitle: "Now Playing"
contentWidth: 280 contentWidth: 280
onDismissed: root.dismissPanel()
C.MprisApplet { C.MprisApplet {
width: hoverPanel.contentWidth width: hoverPanel.contentWidth

View file

@ -4,10 +4,18 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.PinnableSection { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
_panelHovered: hoverPanel.panelHovered tooltip: {
if (state === "wifi")
return "Wi-Fi: " + S.NetworkService.essid;
if (state === "eth")
return "Ethernet: connected";
if (state === "linked")
return "Network: linked";
return "Network: disconnected";
}
readonly property string state: S.NetworkService.state readonly property string state: S.NetworkService.state
@ -23,18 +31,12 @@ M.PinnableSection {
} }
color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
visible: root.state === "wifi" visible: root.state === "wifi"
label: S.NetworkService.essid label: S.NetworkService.essid
color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
Connections { Connections {
@ -53,6 +55,7 @@ M.PinnableSection {
panelNamespace: "nova-network" panelNamespace: "nova-network"
panelTitle: "Wi-Fi" panelTitle: "Wi-Fi"
contentWidth: 250 contentWidth: 250
onDismissed: root.dismissPanel()
titleActionsComponent: Component { titleActionsComponent: Component {
Item { Item {
width: 20 width: 20

View file

@ -5,10 +5,10 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.PinnableSection { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
_panelHovered: hoverPanel.panelHovered tooltip: S.NotifService.count > 0 ? "Notifications: " + S.NotifService.count + (S.NotifService.dnd ? " (DND)" : "") : (S.NotifService.dnd ? "Do not disturb" : "No notifications")
readonly property bool hasUrgent: S.NotifService.list.some(n => n.urgency === NotificationUrgency.Critical && n.state !== "dismissed") readonly property bool hasUrgent: S.NotifService.list.some(n => n.urgency === NotificationUrgency.Critical && n.state !== "dismissed")
@ -20,18 +20,12 @@ M.PinnableSection {
} }
color: S.NotifService.dnd ? S.Theme.base04 : root.accentColor color: S.NotifService.dnd ? S.Theme.base04 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
id: countLabel id: countLabel
label: S.NotifService.count > 0 ? String(S.NotifService.count) + (root.hasUrgent ? "!" : "") : "" label: S.NotifService.count > 0 ? String(S.NotifService.count) + (root.hasUrgent ? "!" : "") : ""
color: root.hasUrgent ? S.Theme.base08 : root.accentColor color: root.hasUrgent ? S.Theme.base08 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
transform: Scale { transform: Scale {
id: countScale id: countScale
@ -83,6 +77,7 @@ M.PinnableSection {
panelNamespace: "nova-notifications" panelNamespace: "nova-notifications"
panelTitle: "Notifications" panelTitle: "Notifications"
contentWidth: 350 contentWidth: 350
onDismissed: root.dismissPanel()
titleActionsComponent: Component { titleActionsComponent: Component {
Row { Row {
spacing: 8 spacing: 8

View file

@ -1,25 +0,0 @@
import QtQuick
// Base component for bar modules with OSD flash behavior (Volume, Backlight).
// Panel shows on hover or when flashPanel() is called, auto-dismisses after 1.5s.
// Modules bind _panelHovered to their HoverPanel's panelHovered property.
BarSection {
id: root
tooltip: ""
property bool _panelHovered: false
property bool _osdActive: false
readonly property bool _anyHover: root._hovered || _panelHovered
readonly property bool _showPanel: _anyHover || _osdActive
function flashPanel() {
_osdActive = true;
_osdTimer.restart();
}
Timer {
id: _osdTimer
interval: 1500
onTriggered: root._osdActive = false
}
}

View file

@ -1,27 +0,0 @@
import QtQuick
// Base component for bar modules with a pinnable hover panel.
// Provides the _pinned/_anyHover/_showPanel/_unpinTimer boilerplate.
// Modules bind _panelHovered to their HoverPanel's panelHovered property.
BarSection {
id: root
tooltip: ""
property bool _pinned: false
property bool _panelHovered: false
readonly property bool _anyHover: root._hovered || _panelHovered
readonly property bool _showPanel: _anyHover || _pinned
on_AnyHoverChanged: {
if (_anyHover)
_unpinTimer.stop();
else if (_pinned)
_unpinTimer.start();
}
Timer {
id: _unpinTimer
interval: 500
onTriggered: root._pinned = false
}
}

View file

@ -3,39 +3,40 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import "." as M import "." as M
import "../services" as S import "../services" as S
import "../applets" as C
M.BarIcon { M.BarModule {
id: root id: root
icon: "\uF011"
tooltip: "Power menu" tooltip: "Power menu"
required property var bar
Process { Process {
id: runner id: runner
} }
MouseArea { M.BarIcon {
anchors.fill: parent icon: "\uF011"
cursorShape: Qt.PointingHandCursor anchors.verticalCenter: parent.verticalCenter
onClicked: {
menuLoader.active = !menuLoader.active;
M.TooltipState.visible = false;
}
} }
LazyLoader { M.HoverPanel {
id: menuLoader id: hoverPanel
active: false showPanel: root._showPanel
M.PowerMenu { screen: QsWindow.window?.screen ?? null
anchorItem: root
accentColor: root.accentColor
panelNamespace: "nova-power"
panelTitle: "Power"
contentWidth: 180
onDismissed: root.dismissPanel()
C.PowerApplet {
width: hoverPanel.contentWidth
accentColor: root.accentColor accentColor: root.accentColor
screen: root.bar.screen
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
onDismissed: menuLoader.active = false
onRunCommand: cmd => { onRunCommand: cmd => {
runner.command = cmd; runner.command = cmd;
runner.running = true; runner.running = true;
} }
onDismiss: root.dismissPanel()
} }
} }
} }

View file

@ -2,12 +2,17 @@ import QtQuick
import "." as M import "." as M
import "../services" as S import "../services" as S
M.BarIcon { M.BarModule {
id: root id: root
tooltip: "Power profile: " + (S.PowerProfileService.profile || "unknown") tooltip: "Power profile: " + (S.PowerProfileService.profile || "unknown")
onTapped: {
const cycle = ["performance", "balanced", "power-saver"];
const idx = cycle.indexOf(S.PowerProfileService.profile);
S.PowerProfileService.set(cycle[(idx + 1) % cycle.length]);
}
M.BarIcon {
color: S.PowerProfileService.profile === "performance" ? S.Theme.base09 : S.PowerProfileService.profile === "power-saver" ? S.Theme.base0B : root.accentColor color: S.PowerProfileService.profile === "performance" ? S.Theme.base09 : S.PowerProfileService.profile === "power-saver" ? S.Theme.base0B : root.accentColor
icon: { icon: {
if (S.PowerProfileService.profile === "performance") if (S.PowerProfileService.profile === "performance")
return "\uF0E7"; return "\uF0E7";
@ -17,14 +22,6 @@ M.BarIcon {
return "\uF24E"; return "\uF24E";
return "\uF0E7"; return "\uF0E7";
} }
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
const cycle = ["performance", "balanced", "power-saver"];
const idx = cycle.indexOf(S.PowerProfileService.profile);
S.PowerProfileService.set(cycle[(idx + 1) % cycle.length]);
}
} }
} }

View file

@ -4,11 +4,11 @@ import Quickshell.Services.Pipewire
import "." as M import "." as M
import "../services" as S import "../services" as S
Row { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
cursorShape: Qt.ArrowCursor
// Only detect active client streams, not hardware sources/devices
readonly property bool _videoCapture: { readonly property bool _videoCapture: {
if (!Pipewire.nodes) if (!Pipewire.nodes)
return false; return false;

View file

@ -4,16 +4,15 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.PinnableSection { M.BarModule {
id: root id: root
spacing: Math.max(1, S.Theme.moduleSpacing - 2) spacing: Math.max(1, S.Theme.moduleSpacing - 2)
_panelHovered: hoverPanel.panelHovered tooltip: "Temperature: " + _temp + "\u00B0C"
readonly property int _warm: S.Modules.temperature.warm || 80 readonly property int _warm: S.Modules.temperature.warm || 80
readonly property int _hot: S.Modules.temperature.hot || 90 readonly property int _hot: S.Modules.temperature.hot || 90
readonly property string _deviceFilter: S.Modules.temperature.device || "" readonly property string _deviceFilter: S.Modules.temperature.device || ""
// If a device filter is set, use that device's temp; otherwise fall back to system max
readonly property int _temp: { readonly property int _temp: {
if (_deviceFilter !== "") { if (_deviceFilter !== "") {
const dev = S.SystemStats.tempDevices.find(d => d.name === _deviceFilter); const dev = S.SystemStats.tempDevices.find(d => d.name === _deviceFilter);
@ -34,18 +33,12 @@ M.PinnableSection {
icon: "\uF2C9" icon: "\uF2C9"
color: root._stateColor color: root._stateColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
label: root._temp + "\u00B0C" label: root._temp + "\u00B0C"
minText: "100\u00B0C" minText: "100\u00B0C"
color: root._stateColor color: root._stateColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.HoverPanel { M.HoverPanel {
@ -57,6 +50,7 @@ M.PinnableSection {
panelNamespace: "nova-temperature" panelNamespace: "nova-temperature"
panelTitle: "Temperature" panelTitle: "Temperature"
contentWidth: 220 contentWidth: 220
onDismissed: root.dismissPanel()
C.TemperatureApplet { C.TemperatureApplet {
width: hoverPanel.contentWidth width: hoverPanel.contentWidth

View file

@ -6,7 +6,7 @@ import "../services" as S
M.HoverPanel { M.HoverPanel {
id: menuWindow id: menuWindow
popupMode: true showPanel: true
required property var handle required property var handle

View file

@ -7,10 +7,11 @@ import Quickshell.Services.SystemTray
import "." as M import "." as M
import "../services" as S import "../services" as S
RowLayout { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing + 2 spacing: S.Theme.moduleSpacing + 2
visible: S.Modules.tray.enable && _trayRepeater.count > 0 visible: S.Modules.tray.enable && _trayRepeater.count > 0
cursorShape: Qt.ArrowCursor
required property var bar required property var bar
property var _activeMenu: null property var _activeMenu: null
@ -69,7 +70,7 @@ RowLayout {
M.ThemedIcon { M.ThemedIcon {
anchors.fill: parent anchors.fill: parent
source: iconItem.modelData.icon source: iconItem.modelData.icon
tint: iconItem._needsAttention ? S.Theme.base08 : (root.parent?.accentColor ?? S.Theme.base05) tint: iconItem._needsAttention ? S.Theme.base08 : root.accentColor
} }
} }
@ -81,7 +82,7 @@ RowLayout {
M.TooltipState.text = tip; M.TooltipState.text = tip;
M.TooltipState.itemX = iconItem.mapToGlobal(iconItem.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0); M.TooltipState.itemX = iconItem.mapToGlobal(iconItem.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
M.TooltipState.screen = QsWindow.window?.screen ?? null; M.TooltipState.screen = QsWindow.window?.screen ?? null;
M.TooltipState.accentColor = root.parent?.accentColor ?? S.Theme.base05; M.TooltipState.accentColor = root.accentColor;
M.TooltipState.visible = true; M.TooltipState.visible = true;
} else if (!hovered) { } else if (!hovered) {
M.TooltipState.visible = false; M.TooltipState.visible = false;
@ -114,7 +115,7 @@ RowLayout {
id: menuLoader id: menuLoader
active: false active: false
M.TrayMenu { M.TrayMenu {
accentColor: root.parent?.accentColor ?? S.Theme.base05 accentColor: root.accentColor
handle: iconItem.modelData.menu handle: iconItem.modelData.menu
screen: root.bar.screen screen: root.bar.screen
anchorX: iconItem.mapToGlobal(iconItem.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) anchorX: iconItem.mapToGlobal(iconItem.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)

View file

@ -5,10 +5,10 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.OsdSection { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
_panelHovered: hoverPanel.panelHovered tooltip: "Volume: " + Math.round(volume * 100) + "%" + (muted ? " (muted)" : "")
PwObjectTracker { PwObjectTracker {
objects: [Pipewire.defaultAudioSink, ...root._streamList] objects: [Pipewire.defaultAudioSink, ...root._streamList]
@ -63,24 +63,12 @@ M.OsdSection {
minIcon: "\uF028" minIcon: "\uF028"
color: root._volumeColor color: root._volumeColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: if (root.sink?.audio)
root.sink.audio.muted = !root.sink.audio.muted
}
} }
M.BarLabel { M.BarLabel {
label: Math.round(root.volume * 100) + "%" label: Math.round(root.volume * 100) + "%"
minText: "100%" minText: "100%"
color: root._volumeColor color: root._volumeColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: if (root.sink?.audio)
root.sink.audio.muted = !root.sink.audio.muted
}
} }
WheelHandler { WheelHandler {
@ -100,6 +88,7 @@ M.OsdSection {
panelNamespace: "nova-volume" panelNamespace: "nova-volume"
panelTitle: "Sound" panelTitle: "Sound"
contentWidth: 220 contentWidth: 220
onDismissed: root.dismissPanel()
C.VolumeApplet { C.VolumeApplet {
width: hoverPanel.contentWidth width: hoverPanel.contentWidth

View file

@ -4,18 +4,15 @@ import "." as M
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
M.PinnableSection { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
visible: S.Modules.weather.enable && S.WeatherService.available visible: S.Modules.weather.enable && S.WeatherService.available
_panelHovered: hoverPanel.panelHovered tooltip: S.WeatherService.summary || "Weather"
M.BarIcon { M.BarIcon {
icon: S.WeatherService.icon icon: S.WeatherService.icon
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.HoverPanel { M.HoverPanel {
@ -27,6 +24,7 @@ M.PinnableSection {
panelNamespace: "nova-weather" panelNamespace: "nova-weather"
panelTitle: "Weather" panelTitle: "Weather"
contentWidth: 280 contentWidth: 280
onDismissed: root.dismissPanel()
C.WeatherApplet { C.WeatherApplet {
width: hoverPanel.contentWidth width: hoverPanel.contentWidth

View file

@ -5,10 +5,11 @@ import Quickshell.Widgets
import "." as M import "." as M
import "../services" as S import "../services" as S
M.BarSection { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
tooltip: S.NiriIpc.focusedAppId ? S.NiriIpc.focusedAppId + "\n" + S.NiriIpc.focusedTitle : S.NiriIpc.focusedTitle tooltip: S.NiriIpc.focusedAppId ? S.NiriIpc.focusedAppId + "\n" + S.NiriIpc.focusedTitle : S.NiriIpc.focusedTitle
cursorShape: Qt.ArrowCursor
readonly property string _iconSource: { readonly property string _iconSource: {
if (!S.NiriIpc.focusedAppId) if (!S.NiriIpc.focusedAppId)
@ -19,7 +20,7 @@ M.BarSection {
readonly property real _iconOffset: _icon.visible ? _icon.width + root.spacing : 0 readonly property real _iconOffset: _icon.visible ? _icon.width + root.spacing : 0
// Natural content width Bar.qml uses this to cap the group width // Natural content width - Bar.qml uses this to cap the group width
readonly property real naturalWidth: _iconOffset + _label.implicitWidth readonly property real naturalWidth: _iconOffset + _label.implicitWidth
IconImage { IconImage {

View file

@ -4,9 +4,10 @@ import Quickshell.Io
import "." as M import "." as M
import "../services" as S import "../services" as S
Row { M.BarModule {
id: root id: root
spacing: 4 spacing: 4
cursorShape: Qt.ArrowCursor
required property var bar required property var bar
@ -71,7 +72,7 @@ Row {
M.TooltipState.text = name; M.TooltipState.text = name;
M.TooltipState.itemX = pill.mapToGlobal(pill.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0); M.TooltipState.itemX = pill.mapToGlobal(pill.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
M.TooltipState.screen = QsWindow.window?.screen ?? null; M.TooltipState.screen = QsWindow.window?.screen ?? null;
M.TooltipState.accentColor = root.parent?.accentColor ?? S.Theme.base05; M.TooltipState.accentColor = root.accentColor;
M.TooltipState.visible = true; M.TooltipState.visible = true;
} else { } else {
M.TooltipState.visible = false; M.TooltipState.visible = false;
@ -82,7 +83,7 @@ Row {
width: S.Theme.fontSize + 4 width: S.Theme.fontSize + 4
height: S.Theme.fontSize + 4 height: S.Theme.fontSize + 4
radius: width / 2 radius: width / 2
color: pill.active ? (root.parent?.accentColor ?? S.Theme.base0D) : (pill._hovered ? S.Theme.base03 : S.Theme.base02) color: pill.active ? root.accentColor : (pill._hovered ? S.Theme.base03 : S.Theme.base02)
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: 150 duration: 150
@ -92,7 +93,7 @@ Row {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: pill.modelData.idx text: pill.modelData.idx
color: pill.active ? S.Theme.base00 : (root.parent?.accentColor ?? S.Theme.base05) color: pill.active ? S.Theme.base00 : root.accentColor
font.pixelSize: S.Theme.fontSize - 2 font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily font.family: S.Theme.fontFamily
font.bold: pill.active font.bold: pill.active

View file

@ -6,7 +6,7 @@ Bar 1.0 Bar.qml
BarGroup 1.0 BarGroup.qml BarGroup 1.0 BarGroup.qml
BarIcon 1.0 BarIcon.qml BarIcon 1.0 BarIcon.qml
BarLabel 1.0 BarLabel.qml BarLabel 1.0 BarLabel.qml
BarSection 1.0 BarSection.qml BarModule 1.0 BarModule.qml
BatteryModule 1.0 BatteryModule.qml BatteryModule 1.0 BatteryModule.qml
BluetoothModule 1.0 BluetoothModule.qml BluetoothModule 1.0 BluetoothModule.qml
ClockModule 1.0 ClockModule.qml ClockModule 1.0 ClockModule.qml
@ -21,11 +21,8 @@ NetworkModule 1.0 NetworkModule.qml
NotifCard 1.0 NotifCard.qml NotifCard 1.0 NotifCard.qml
NotifPopup 1.0 NotifPopup.qml NotifPopup 1.0 NotifPopup.qml
NotificationsModule 1.0 NotificationsModule.qml NotificationsModule 1.0 NotificationsModule.qml
OsdSection 1.0 OsdSection.qml
OverviewBackdrop 1.0 OverviewBackdrop.qml OverviewBackdrop 1.0 OverviewBackdrop.qml
PinnableSection 1.0 PinnableSection.qml
PopupBackground 1.0 PopupBackground.qml PopupBackground 1.0 PopupBackground.qml
PowerMenu 1.0 PowerMenu.qml
PowerModule 1.0 PowerModule.qml PowerModule 1.0 PowerModule.qml
PowerProfileModule 1.0 PowerProfileModule.qml PowerProfileModule 1.0 PowerProfileModule.qml
PrivacyModule 1.0 PrivacyModule.qml PrivacyModule 1.0 PrivacyModule.qml