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
commit 26476dc930
32 changed files with 265 additions and 509 deletions

View file

@ -4,12 +4,12 @@ import "." as M
import "../services" as S
import "../applets" as C
M.OsdSection {
M.BarModule {
id: root
spacing: S.Theme.moduleSpacing
opacity: S.Modules.backlight.enable && S.BacklightService.available ? 1 : 0
visible: opacity > 0
_panelHovered: hoverPanel.panelHovered
tooltip: "Brightness: " + percent + "%"
property int percent: S.BacklightService.percent
property bool _percentInit: false
@ -27,24 +27,6 @@ M.OsdSection {
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 {
icon: "\uF185"
anchors.verticalCenter: parent.verticalCenter
@ -54,4 +36,23 @@ M.OsdSection {
minText: "100%"
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
M.BatteryModule {}
M.PowerModule {
bar: bar
visible: S.Modules.power.enable
}
}

View file

@ -1,15 +1,13 @@
import QtQuick
import Quickshell
import "." as M
import "../services" as S
// Icon element with crossfade animation on icon change.
// Pure visual component - tooltip handling lives in the parent BarModule.
Text {
id: root
property string icon: ""
property string tooltip: ""
property string minIcon: ""
property color accentColor: parent?.accentColor ?? S.Theme.base05
property bool _hovered: false
property string _displayIcon: icon
property string _pendingIcon: ""
@ -54,23 +52,4 @@ Text {
font.pixelSize: root.font.pixelSize
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 Quickshell
import "." as M
import "../services" as S
// Label element with minimum-width support via minText.
// Pure visual component - tooltip handling lives in the parent BarModule.
Text {
id: root
property string label: ""
property string tooltip: ""
property string minText: ""
property color accentColor: parent?.accentColor ?? S.Theme.base05
property bool _hovered: false
text: label
width: minText ? Math.max(implicitWidth, _minMetrics.width) : implicitWidth
@ -25,23 +23,4 @@ Text {
font.pixelSize: root.font.pixelSize
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 "../applets" as C
M.PinnableSection {
M.BarModule {
id: root
spacing: S.Theme.moduleSpacing
opacity: S.Modules.battery.enable && S.BatteryService.available ? 1 : 0
visible: opacity > 0
_panelHovered: hoverPanel.panelHovered
tooltip: "Battery: " + Math.round(S.BatteryService.percent) + "%" + (S.BatteryService.charging ? " (charging)" : "")
property real _blinkOpacity: 1
@ -18,7 +18,6 @@ M.PinnableSection {
minOpacity: 0.45
}
// Bar widgets
M.BarIcon {
icon: {
if (S.BatteryService.charging)
@ -30,9 +29,6 @@ M.PinnableSection {
opacity: root._blinkOpacity
font.pixelSize: S.Theme.fontSize + 2
anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
}
M.BarLabel {
label: Math.round(S.BatteryService.percent) + "%"
@ -40,12 +36,8 @@ M.PinnableSection {
color: S.BatteryService.stateColor
opacity: root._blinkOpacity
anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
}
// Hover panel
M.HoverPanel {
id: hoverPanel
showPanel: root._showPanel
@ -55,6 +47,7 @@ M.PinnableSection {
panelNamespace: "nova-battery"
panelTitle: "Battery"
contentWidth: 240
onDismissed: root.dismissPanel()
C.BatteryApplet {
width: hoverPanel.contentWidth

View file

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

View file

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

View file

@ -4,10 +4,10 @@ import "." as M
import "../services" as S
import "../applets" as C
M.PinnableSection {
M.BarModule {
id: root
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 _coreMaxFreq: S.SystemStats.cpuCoreMaxFreq
@ -34,17 +34,11 @@ M.PinnableSection {
M.BarIcon {
icon: "\uF2DB"
anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
}
M.BarLabel {
label: S.SystemStats.cpuUsage.toString().padStart(2) + "%@" + S.SystemStats.cpuFreqGhz.toFixed(2)
minText: "99%@9.99"
anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
}
M.HoverPanel {
@ -56,6 +50,7 @@ M.PinnableSection {
panelNamespace: "nova-cpu"
panelTitle: "CPU"
contentWidth: 260
onDismissed: root.dismissPanel()
C.CpuApplet {
width: hoverPanel.contentWidth

View file

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

View file

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

View file

@ -4,33 +4,19 @@ import Quickshell.Wayland
import "." as M
import "../services" as S
// Unified bar panel — fullscreen transparent window so content can resize
// freely without triggering Wayland surface resizes.
// Bar panel - fullscreen transparent window so content can resize 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).
// Parent drives visibility via showPanel. Click-outside or Esc dismisses
// and emits dismissed(). Pass anchorItem for lazy position computation.
PanelWindow {
id: root
property bool popupMode: false
// Hover mode
property bool showPanel: true
property bool showPanel: false
property Item anchorItem: null
property bool panelHovered: false
// Popup mode
property real anchorX: -1
signal dismissed
// Shared
required property color accentColor
property string panelTitle: ""
property Component titleActionsComponent: null
@ -43,22 +29,6 @@ PanelWindow {
color: "transparent"
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.exclusiveZone: 0
@ -80,38 +50,29 @@ PanelWindow {
if (root.anchorItem) {
const pt = root.anchorItem.mapToGlobal(root.anchorItem.width / 2, 0);
cx = pt.x - (scr?.x ?? 0);
} else {
} else if (root.anchorX >= 0) {
cx = root.anchorX;
} else {
cx = sw / 2;
}
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
// to route wl_pointer.enter to the new overlay surface (cursor may be stationary).
// 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.
// Grace period: after _show(), suppress click-outside dismiss briefly so
// Niri has time to route wl_pointer.enter to the new overlay surface.
property bool _grace: false
Timer {
id: _graceTimer
interval: root.popupMode ? 1500 : 400
onTriggered: {
root._grace = false;
if (!root.showPanel && !root._pinned)
root.dismiss();
}
interval: 400
onTriggered: root._grace = false
}
// Content-change grace: call keepOpen(ms) when panel content is about to
// resize/rebuild (session switch, device list change, etc.) to prevent the
// hover-drop-on-resize from closing the panel.
// resize/rebuild (session switch, device list change, etc.).
property bool _contentBusy: false
Timer {
id: _contentBusyTimer
onTriggered: {
root._contentBusy = false;
if (!root.showPanel && !root._grace && !root._pinned)
_hideTimer.restart();
}
onTriggered: root._contentBusy = false
}
function keepOpen(ms) {
_contentBusy = true;
@ -121,13 +82,7 @@ PanelWindow {
function _show() {
_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) {
// Explicitly set y before animating — avoids the y:-height binding (live, depends on
// _panelColumn.height) surviving a 0→0 no-op animation when layout isn't done yet.
panelContainer.y = -(panelContainer.height > 0 ? panelContainer.height : 400);
panelContainer.opacity = 0;
}
@ -144,12 +99,12 @@ PanelWindow {
}
function dismiss() {
_pinned = false;
if (!_winVisible)
return;
showAnim.stop();
if (S.Theme.reducedMotion) {
_winVisible = false;
if (popupMode)
dismissed();
dismissed();
} else {
hideAnim.start();
}
@ -157,30 +112,12 @@ PanelWindow {
_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: {
if (root.popupMode)
return;
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)
_show();
} else {
_hideTimer.restart();
dismiss();
}
}
@ -220,17 +157,15 @@ PanelWindow {
}
onFinished: {
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.
// 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 {
anchors.fill: parent
visible: root.popupMode
TapHandler {
enabled: !root._grace
@ -243,6 +178,13 @@ PanelWindow {
}
}
// Esc dismiss
Shortcut {
sequence: "Escape"
enabled: root._winVisible
onActivated: root.dismiss()
}
M.PopupBackground {
x: panelContainer.x
y: panelContainer.y
@ -260,52 +202,17 @@ PanelWindow {
height: _panelColumn.height
opacity: 0
HoverHandler {
enabled: !root.popupMode && !root._pinned
onHoveredChanged: if (!root.popupMode && !root._pinned)
root.panelHovered = hovered
}
Column {
id: _panelColumn
width: root.contentWidth
// Header row: title + action buttons + pin — shown in hover mode always,
// and in popup mode when a title or actions are provided.
// Header row: title + action buttons
Item {
id: _headerItem
visible: !root.popupMode || root.panelTitle !== "" || root.titleActionsComponent !== null
visible: root.panelTitle !== "" || root.titleActionsComponent !== null
width: parent.width
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 {
visible: root.panelTitle !== ""
anchors.left: parent.left
@ -318,52 +225,14 @@ PanelWindow {
font.family: S.Theme.fontFamily
}
// Action buttons — anchored left of pin button slot
Loader {
id: _titleActionsLoader
anchors.right: _pinBtn.left
anchors.right: parent.right
anchors.rightMargin: 4
anchors.verticalCenter: parent.verticalCenter
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 {
anchors.left: parent.left
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 {
x: panelContainer.x
y: panelContainer.y

View file

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

View file

@ -4,10 +4,10 @@ import "." as M
import "../services" as S
import "../applets" as C
M.PinnableSection {
M.BarModule {
id: root
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 real usedGb: S.SystemStats.memUsedGb
@ -25,17 +25,11 @@ M.PinnableSection {
M.BarIcon {
icon: "\uEFC5"
anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
}
M.BarLabel {
label: root.percent + "%"
minText: "100%"
anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
}
M.HoverPanel {
@ -47,6 +41,7 @@ M.PinnableSection {
panelNamespace: "nova-memory"
panelTitle: "Memory"
contentWidth: 240
onDismissed: root.dismissPanel()
C.MemoryApplet {
width: hoverPanel.contentWidth

View file

@ -6,12 +6,12 @@ import "." as M
import "../services" as S
import "../applets" as C
M.PinnableSection {
M.BarModule {
id: root
spacing: S.Theme.moduleSpacing
opacity: S.Modules.mpris.enable && player !== null ? 1 : 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 MprisPlayer player: S.MprisService.player
@ -19,7 +19,6 @@ M.PinnableSection {
property string _cachedArt: ""
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 _currentTrack: player?.trackTitle ?? ""
on_ArtUrlChanged: if (_artUrl)
@ -74,22 +73,12 @@ M.PinnableSection {
M.BarIcon {
icon: root.playing ? "\uF04B" : (root.player?.playbackState === MprisPlaybackState.Paused ? "\uDB80\uDFE4" : "\uDB81\uDCDB")
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root._pinned = !root._pinned
}
}
M.BarLabel {
label: root.player?.trackTitle || root.player?.identity || ""
elide: Text.ElideRight
width: Math.min(implicitWidth, 200)
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root._pinned = !root._pinned
}
}
M.HoverPanel {
@ -101,6 +90,7 @@ M.PinnableSection {
panelNamespace: "nova-mpris"
panelTitle: "Now Playing"
contentWidth: 280
onDismissed: root.dismissPanel()
C.MprisApplet {
width: hoverPanel.contentWidth

View file

@ -4,10 +4,18 @@ import "." as M
import "../services" as S
import "../applets" as C
M.PinnableSection {
M.BarModule {
id: root
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
@ -23,18 +31,12 @@ M.PinnableSection {
}
color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor
anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
}
M.BarLabel {
visible: root.state === "wifi"
label: S.NetworkService.essid
color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor
anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
}
Connections {
@ -53,6 +55,7 @@ M.PinnableSection {
panelNamespace: "nova-network"
panelTitle: "Wi-Fi"
contentWidth: 250
onDismissed: root.dismissPanel()
titleActionsComponent: Component {
Item {
width: 20

View file

@ -5,10 +5,10 @@ import "." as M
import "../services" as S
import "../applets" as C
M.PinnableSection {
M.BarModule {
id: root
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")
@ -20,18 +20,12 @@ M.PinnableSection {
}
color: S.NotifService.dnd ? S.Theme.base04 : root.accentColor
anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
}
M.BarLabel {
id: countLabel
label: S.NotifService.count > 0 ? String(S.NotifService.count) + (root.hasUrgent ? "!" : "") : ""
color: root.hasUrgent ? S.Theme.base08 : root.accentColor
anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
transform: Scale {
id: countScale
@ -83,6 +77,7 @@ M.PinnableSection {
panelNamespace: "nova-notifications"
panelTitle: "Notifications"
contentWidth: 350
onDismissed: root.dismissPanel()
titleActionsComponent: Component {
Row {
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
}
}

Some files were not shown because too many files have changed in this diff Show more