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 Quickshell
import "." as M
import "../services" as S
M.HoverPanel {
id: menuWindow
popupMode: true
contentWidth: 180
Column {
id: root
required property color accentColor
signal runCommand(var cmd)
signal dismiss
readonly property bool _isNiri: Quickshell.env("NIRI_SOCKET") !== ""
// Confirmation state: null = normal menu, object = pending confirm
property var _confirmItem: null
function _run(cmd) {
@ -35,8 +32,8 @@ M.HoverPanel {
// Normal menu entries
Column {
visible: !menuWindow._confirmItem
width: menuWindow.contentWidth
visible: !root._confirmItem
width: root.width
Repeater {
model: [
@ -57,7 +54,7 @@ M.HoverPanel {
{
label: "Logout",
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,
confirm: false
},
@ -83,7 +80,7 @@ M.HoverPanel {
required property var modelData
required property int index
width: menuWindow.contentWidth
width: root.width
height: 32
Rectangle {
@ -121,7 +118,7 @@ M.HoverPanel {
}
TapHandler {
onTapped: menuWindow._requestAction(entry.modelData)
onTapped: root._requestAction(entry.modelData)
}
}
}
@ -129,8 +126,8 @@ M.HoverPanel {
// Confirmation view
Column {
visible: !!menuWindow._confirmItem
width: menuWindow.contentWidth
visible: !!root._confirmItem
width: root.width
spacing: 4
Item {
@ -139,8 +136,8 @@ M.HoverPanel {
Text {
anchors.centerIn: parent
text: menuWindow._confirmItem ? menuWindow._confirmItem.label + "?" : ""
color: menuWindow._confirmItem ? menuWindow._confirmItem.color : S.Theme.base05
text: root._confirmItem ? root._confirmItem.label + "?" : ""
color: root._confirmItem ? root._confirmItem.color : S.Theme.base05
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.fontFamily
font.bold: true
@ -151,7 +148,6 @@ M.HoverPanel {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 8
// Cancel
Item {
width: 72
height: 28
@ -178,11 +174,10 @@ M.HoverPanel {
}
TapHandler {
onTapped: menuWindow._cancelConfirm()
onTapped: root._cancelConfirm()
}
}
// Confirm
Item {
width: 72
height: 28
@ -190,20 +185,20 @@ M.HoverPanel {
Rectangle {
anchors.fill: parent
color: {
if (!menuWindow._confirmItem)
if (!root._confirmItem)
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);
}
radius: S.Theme.radius
border.width: 1
border.color: menuWindow._confirmItem ? menuWindow._confirmItem.color : S.Theme.base03
border.color: root._confirmItem ? root._confirmItem.color : S.Theme.base03
}
Text {
anchors.centerIn: parent
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.family: S.Theme.fontFamily
font.bold: true
@ -216,8 +211,8 @@ M.HoverPanel {
TapHandler {
onTapped: {
if (menuWindow._confirmItem)
menuWindow._run(menuWindow._confirmItem.cmd);
if (root._confirmItem)
root._run(root._confirmItem.cmd);
}
}
}

View file

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

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 00 no-op animation when layout isn't done yet.
panelContainer.y = -(panelContainer.height > 0 ? panelContainer.height : 400);
panelContainer.opacity = 0;
}
@ -144,11 +99,11 @@ PanelWindow {
}
function dismiss() {
_pinned = false;
if (!_winVisible)
return;
showAnim.stop();
if (S.Theme.reducedMotion) {
_winVisible = false;
if (popupMode)
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();
}
}
// 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
}
}

View file

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

View file

@ -2,12 +2,17 @@ import QtQuick
import "." as M
import "../services" as S
M.BarIcon {
M.BarModule {
id: root
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
icon: {
if (S.PowerProfileService.profile === "performance")
return "\uF0E7";
@ -17,14 +22,6 @@ M.BarIcon {
return "\uF24E";
return "\uF0E7";
}
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]);
}
anchors.verticalCenter: parent.verticalCenter
}
}

View file

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

View file

@ -4,16 +4,15 @@ 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: "Temperature: " + _temp + "\u00B0C"
readonly property int _warm: S.Modules.temperature.warm || 80
readonly property int _hot: S.Modules.temperature.hot || 90
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: {
if (_deviceFilter !== "") {
const dev = S.SystemStats.tempDevices.find(d => d.name === _deviceFilter);
@ -34,18 +33,12 @@ M.PinnableSection {
icon: "\uF2C9"
color: root._stateColor
anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
}
M.BarLabel {
label: root._temp + "\u00B0C"
minText: "100\u00B0C"
color: root._stateColor
anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
}
M.HoverPanel {
@ -57,6 +50,7 @@ M.PinnableSection {
panelNamespace: "nova-temperature"
panelTitle: "Temperature"
contentWidth: 220
onDismissed: root.dismissPanel()
C.TemperatureApplet {
width: hoverPanel.contentWidth

View file

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

View file

@ -7,10 +7,11 @@ import Quickshell.Services.SystemTray
import "." as M
import "../services" as S
RowLayout {
M.BarModule {
id: root
spacing: S.Theme.moduleSpacing + 2
visible: S.Modules.tray.enable && _trayRepeater.count > 0
cursorShape: Qt.ArrowCursor
required property var bar
property var _activeMenu: null
@ -69,7 +70,7 @@ RowLayout {
M.ThemedIcon {
anchors.fill: parent
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.itemX = iconItem.mapToGlobal(iconItem.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
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;
} else if (!hovered) {
M.TooltipState.visible = false;
@ -114,7 +115,7 @@ RowLayout {
id: menuLoader
active: false
M.TrayMenu {
accentColor: root.parent?.accentColor ?? S.Theme.base05
accentColor: root.accentColor
handle: iconItem.modelData.menu
screen: root.bar.screen
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 "../applets" as C
M.OsdSection {
M.BarModule {
id: root
spacing: S.Theme.moduleSpacing
_panelHovered: hoverPanel.panelHovered
tooltip: "Volume: " + Math.round(volume * 100) + "%" + (muted ? " (muted)" : "")
PwObjectTracker {
objects: [Pipewire.defaultAudioSink, ...root._streamList]
@ -63,24 +63,12 @@ M.OsdSection {
minIcon: "\uF028"
color: root._volumeColor
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 {
label: Math.round(root.volume * 100) + "%"
minText: "100%"
color: root._volumeColor
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 {
@ -100,6 +88,7 @@ M.OsdSection {
panelNamespace: "nova-volume"
panelTitle: "Sound"
contentWidth: 220
onDismissed: root.dismissPanel()
C.VolumeApplet {
width: hoverPanel.contentWidth

View file

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

View file

@ -5,10 +5,11 @@ import Quickshell.Widgets
import "." as M
import "../services" as S
M.BarSection {
M.BarModule {
id: root
spacing: S.Theme.moduleSpacing
tooltip: S.NiriIpc.focusedAppId ? S.NiriIpc.focusedAppId + "\n" + S.NiriIpc.focusedTitle : S.NiriIpc.focusedTitle
cursorShape: Qt.ArrowCursor
readonly property string _iconSource: {
if (!S.NiriIpc.focusedAppId)
@ -19,7 +20,7 @@ M.BarSection {
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
IconImage {

View file

@ -4,9 +4,10 @@ import Quickshell.Io
import "." as M
import "../services" as S
Row {
M.BarModule {
id: root
spacing: 4
cursorShape: Qt.ArrowCursor
required property var bar
@ -71,7 +72,7 @@ Row {
M.TooltipState.text = name;
M.TooltipState.itemX = pill.mapToGlobal(pill.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
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;
} else {
M.TooltipState.visible = false;
@ -82,7 +83,7 @@ Row {
width: S.Theme.fontSize + 4
height: S.Theme.fontSize + 4
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 {
ColorAnimation {
duration: 150
@ -92,7 +93,7 @@ Row {
Text {
anchors.centerIn: parent
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.family: S.Theme.fontFamily
font.bold: pill.active

View file

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