diff --git a/shell/modules/PowerMenu.qml b/shell/applets/PowerApplet.qml similarity index 81% rename from shell/modules/PowerMenu.qml rename to shell/applets/PowerApplet.qml index 4a36105..e444691 100644 --- a/shell/modules/PowerMenu.qml +++ b/shell/applets/PowerApplet.qml @@ -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); } } } diff --git a/shell/applets/qmldir b/shell/applets/qmldir index b6e1e2d..a9935e7 100644 --- a/shell/applets/qmldir +++ b/shell/applets/qmldir @@ -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 diff --git a/shell/modules/BacklightModule.qml b/shell/modules/BacklightModule.qml index d3e0b5b..5a531d6 100644 --- a/shell/modules/BacklightModule.qml +++ b/shell/modules/BacklightModule.qml @@ -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) + } + } } diff --git a/shell/modules/Bar.qml b/shell/modules/Bar.qml index 0458381..d7a2c9b 100644 --- a/shell/modules/Bar.qml +++ b/shell/modules/Bar.qml @@ -221,7 +221,6 @@ PanelWindow { rightEdge: true M.BatteryModule {} M.PowerModule { - bar: bar visible: S.Modules.power.enable } } diff --git a/shell/modules/BarIcon.qml b/shell/modules/BarIcon.qml index ebad597..c60df0d 100644 --- a/shell/modules/BarIcon.qml +++ b/shell/modules/BarIcon.qml @@ -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 } diff --git a/shell/modules/BarLabel.qml b/shell/modules/BarLabel.qml index b794001..b27beb4 100644 --- a/shell/modules/BarLabel.qml +++ b/shell/modules/BarLabel.qml @@ -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 } diff --git a/shell/modules/BarModule.qml b/shell/modules/BarModule.qml new file mode 100644 index 0000000..8915880 --- /dev/null +++ b/shell/modules/BarModule.qml @@ -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 + } + } +} diff --git a/shell/modules/BarSection.qml b/shell/modules/BarSection.qml deleted file mode 100644 index 1082e5d..0000000 --- a/shell/modules/BarSection.qml +++ /dev/null @@ -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 -} diff --git a/shell/modules/BatteryModule.qml b/shell/modules/BatteryModule.qml index 3d0bf08..309d46d 100644 --- a/shell/modules/BatteryModule.qml +++ b/shell/modules/BatteryModule.qml @@ -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 diff --git a/shell/modules/BluetoothModule.qml b/shell/modules/BluetoothModule.qml index 91b0bc5..e2710c5 100644 --- a/shell/modules/BluetoothModule.qml +++ b/shell/modules/BluetoothModule.qml @@ -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 diff --git a/shell/modules/ClockModule.qml b/shell/modules/ClockModule.qml index 69e3663..baffffb 100644 --- a/shell/modules/ClockModule.qml +++ b/shell/modules/ClockModule.qml @@ -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 diff --git a/shell/modules/CpuModule.qml b/shell/modules/CpuModule.qml index 16e5db6..68ed553 100644 --- a/shell/modules/CpuModule.qml +++ b/shell/modules/CpuModule.qml @@ -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 diff --git a/shell/modules/DiskModule.qml b/shell/modules/DiskModule.qml index 616ed65..0763b9b 100644 --- a/shell/modules/DiskModule.qml +++ b/shell/modules/DiskModule.qml @@ -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 diff --git a/shell/modules/GpuModule.qml b/shell/modules/GpuModule.qml index 794ad55..634cb10 100644 --- a/shell/modules/GpuModule.qml +++ b/shell/modules/GpuModule.qml @@ -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 diff --git a/shell/modules/HoverPanel.qml b/shell/modules/HoverPanel.qml index 5f7adc5..c6b6894 100644 --- a/shell/modules/HoverPanel.qml +++ b/shell/modules/HoverPanel.qml @@ -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 diff --git a/shell/modules/IdleInhibitorModule.qml b/shell/modules/IdleInhibitorModule.qml index 4cda17d..e7f54e5 100644 --- a/shell/modules/IdleInhibitorModule.qml +++ b/shell/modules/IdleInhibitorModule.qml @@ -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 } } diff --git a/shell/modules/MemoryModule.qml b/shell/modules/MemoryModule.qml index d1d54bc..60b74bb 100644 --- a/shell/modules/MemoryModule.qml +++ b/shell/modules/MemoryModule.qml @@ -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 diff --git a/shell/modules/MprisModule.qml b/shell/modules/MprisModule.qml index cd32262..4596da8 100644 --- a/shell/modules/MprisModule.qml +++ b/shell/modules/MprisModule.qml @@ -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 diff --git a/shell/modules/NetworkModule.qml b/shell/modules/NetworkModule.qml index df827c2..fb08ffe 100644 --- a/shell/modules/NetworkModule.qml +++ b/shell/modules/NetworkModule.qml @@ -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 diff --git a/shell/modules/NotificationsModule.qml b/shell/modules/NotificationsModule.qml index 044dc41..7bb9075 100644 --- a/shell/modules/NotificationsModule.qml +++ b/shell/modules/NotificationsModule.qml @@ -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 diff --git a/shell/modules/OsdSection.qml b/shell/modules/OsdSection.qml deleted file mode 100644 index bfddde8..0000000 --- a/shell/modules/OsdSection.qml +++ /dev/null @@ -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 - } -} diff --git a/shell/modules/PinnableSection.qml b/shell/modules/PinnableSection.qml deleted file mode 100644 index 12aff05..0000000 --- a/shell/modules/PinnableSection.qml +++ /dev/null @@ -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 - } -} diff --git a/shell/modules/PowerModule.qml b/shell/modules/PowerModule.qml index 6321b2b..31a5407 100644 --- a/shell/modules/PowerModule.qml +++ b/shell/modules/PowerModule.qml @@ -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() } } } diff --git a/shell/modules/PowerProfileModule.qml b/shell/modules/PowerProfileModule.qml index 58e61e9..d2e31af 100644 --- a/shell/modules/PowerProfileModule.qml +++ b/shell/modules/PowerProfileModule.qml @@ -2,29 +2,26 @@ import QtQuick import "." as M import "../services" as S -M.BarIcon { +M.BarModule { id: root tooltip: "Power profile: " + (S.PowerProfileService.profile || "unknown") - - 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"; - if (S.PowerProfileService.profile === "power-saver") - return "\uF06C"; - if (S.PowerProfileService.profile === "balanced") - return "\uF24E"; - return "\uF0E7"; + onTapped: { + const cycle = ["performance", "balanced", "power-saver"]; + const idx = cycle.indexOf(S.PowerProfileService.profile); + S.PowerProfileService.set(cycle[(idx + 1) % cycle.length]); } - 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]); + 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"; + if (S.PowerProfileService.profile === "power-saver") + return "\uF06C"; + if (S.PowerProfileService.profile === "balanced") + return "\uF24E"; + return "\uF0E7"; } + anchors.verticalCenter: parent.verticalCenter } } diff --git a/shell/modules/PrivacyModule.qml b/shell/modules/PrivacyModule.qml index 3f8b84f..fd4adaa 100644 --- a/shell/modules/PrivacyModule.qml +++ b/shell/modules/PrivacyModule.qml @@ -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; diff --git a/shell/modules/TemperatureModule.qml b/shell/modules/TemperatureModule.qml index b352759..f355910 100644 --- a/shell/modules/TemperatureModule.qml +++ b/shell/modules/TemperatureModule.qml @@ -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 diff --git a/shell/modules/TrayMenu.qml b/shell/modules/TrayMenu.qml index 922aa0f..d663661 100644 --- a/shell/modules/TrayMenu.qml +++ b/shell/modules/TrayMenu.qml @@ -6,7 +6,7 @@ import "../services" as S M.HoverPanel { id: menuWindow - popupMode: true + showPanel: true required property var handle diff --git a/shell/modules/TrayModule.qml b/shell/modules/TrayModule.qml index 647adce..5077652 100644 --- a/shell/modules/TrayModule.qml +++ b/shell/modules/TrayModule.qml @@ -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) diff --git a/shell/modules/VolumeModule.qml b/shell/modules/VolumeModule.qml index ed193bc..8e71f45 100644 --- a/shell/modules/VolumeModule.qml +++ b/shell/modules/VolumeModule.qml @@ -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 diff --git a/shell/modules/WeatherModule.qml b/shell/modules/WeatherModule.qml index b22b24c..ba06651 100644 --- a/shell/modules/WeatherModule.qml +++ b/shell/modules/WeatherModule.qml @@ -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 diff --git a/shell/modules/WindowTitleModule.qml b/shell/modules/WindowTitleModule.qml index 98b1215..d089c57 100644 --- a/shell/modules/WindowTitleModule.qml +++ b/shell/modules/WindowTitleModule.qml @@ -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 { diff --git a/shell/modules/WorkspacesModule.qml b/shell/modules/WorkspacesModule.qml index 6da1cfe..03aac54 100644 --- a/shell/modules/WorkspacesModule.qml +++ b/shell/modules/WorkspacesModule.qml @@ -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 diff --git a/shell/modules/qmldir b/shell/modules/qmldir index d52222c..33d7e9f 100644 --- a/shell/modules/qmldir +++ b/shell/modules/qmldir @@ -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