Compare commits

...
4 changed files with 119 additions and 23 deletions

View file

@ -43,14 +43,16 @@ PanelWindow {
property bool _winVisible: false
property bool _pinned: false
property real _dragStartX: 0
// When pinned, only the pin button area receives input everything else passes through
// When pinned, only the header bar receives input content passes through to desktop.
// Full header width allows drag-to-reposition; pin button tap is within that region too.
mask: _pinned ? _pinMask : null
property Region _pinMask: Region {
x: panelContainer.x + panelContainer.width - 28
x: panelContainer.x
y: 0
width: 28
width: panelContainer.width
height: 24
}
@ -190,12 +192,21 @@ PanelWindow {
}
}
// Popup mode: click-outside dismiss (declared first = lowest z = below content)
MouseArea {
// Popup mode: 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.
Item {
anchors.fill: parent
visible: root.popupMode
enabled: root.popupMode
onClicked: root.dismiss()
TapHandler {
enabled: !root._grace
onTapped: {
const p = point.position;
if (p.x < panelContainer.x || p.x > panelContainer.x + panelContainer.width || p.y < panelContainer.y || p.y > panelContainer.y + panelContainer.height)
root.dismiss();
}
}
}
M.PopupBackground {
@ -215,13 +226,6 @@ PanelWindow {
height: _panelColumn.height
opacity: 0
// Popup mode: eat clicks on panel background so outer dismiss doesn't fire
MouseArea {
anchors.fill: parent
visible: root.popupMode
enabled: root.popupMode
}
HoverHandler {
enabled: !root.popupMode && !root._pinned
onHoveredChanged: if (!root.popupMode && !root._pinned)
@ -235,10 +239,31 @@ PanelWindow {
// Header row: title + action buttons + pin shown in hover mode always,
// and in popup mode when a title or actions are provided.
Item {
id: _headerItem
visible: !root.popupMode || root.panelTitle !== "" || root.titleActionsComponent !== null
width: parent.width
height: 24
// Drag header to reposition panel while pinned (hover mode only)
DragHandler {
enabled: root._pinned && !root.popupMode
yAxis.enabled: false
onActiveChanged: if (active)
root._dragStartX = panelContainer.x
onActiveTranslationChanged: {
if (active) {
const sw = root.screen?.width ?? 1920;
panelContainer.x = Math.max(0, Math.min(root._dragStartX + activeTranslation.x, sw - root.contentWidth));
}
}
}
// Show move cursor on header when pinned
HoverHandler {
enabled: root._pinned && !root.popupMode
cursorShape: Qt.SizeHorCursor
}
Text {
visible: root.panelTitle !== ""
anchors.left: parent.left

View file

@ -122,7 +122,7 @@ M.BarSection {
panelTitle: "Now Playing"
contentWidth: 280
// Album art always 1:1
// Album art always 1:1, crossfades on session switch
Item {
width: parent.width
height: width
@ -133,22 +133,71 @@ M.BarSection {
color: M.Theme.base02
}
// Outgoing art snaps to current opacity, then fades out
Image {
id: _artImgPrev
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
asynchronous: true
opacity: 0
NumberAnimation {
id: _prevFadeOut
target: _artImgPrev
property: "opacity"
to: 0
duration: 300
easing.type: Easing.InOutCubic
}
}
// Incoming art fades in once loaded
Image {
id: _artImg
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
visible: _hasArt
asynchronous: true
source: root._cachedArt
opacity: 0
property bool _hasArt: false
onStatusChanged: if (status === Image.Ready)
_hasArt = true
onStatusChanged: {
if (status === Image.Ready && source !== "") {
_hasArt = true;
_artFadeIn.start();
_prevFadeOut.start();
} else if (status === Image.Error) {
_hasArt = false;
}
}
NumberAnimation {
id: _artFadeIn
target: _artImg
property: "opacity"
to: 1
duration: 300
easing.type: Easing.InOutCubic
}
Connections {
target: root
function on_CachedArtChanged() {
if (!root._cachedArt)
if (!root._cachedArt) {
_artFadeIn.stop();
_prevFadeOut.stop();
_artImg._hasArt = false;
_artImg.opacity = 0;
_artImgPrev.opacity = 0;
_artImg.source = "";
} else if (root._cachedArt !== _artImg.source) {
_prevFadeOut.stop();
_artFadeIn.stop();
_artImgPrev.source = _artImg.source;
_artImgPrev.opacity = _artImg.opacity;
_artImg.opacity = 0;
_artImg.source = root._cachedArt;
}
}
}
}
@ -187,7 +236,7 @@ M.BarSection {
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 40
visible: _artImg.visible
visible: _artImg._hasArt
gradient: Gradient {
GradientStop {
position: 0
@ -206,7 +255,7 @@ M.BarSection {
color: M.Theme.base04
font.pixelSize: 28
font.family: M.Theme.iconFontFamily
visible: _artImg.status !== Image.Ready
visible: !_artImg._hasArt
}
}

View file

@ -90,7 +90,13 @@ QtObject {
});
root._byId[item.id] = item;
root.list = [item, ...root.list];
root.list = [item, ...root.list].sort((a, b) => {
const aU = a.urgency === NotificationUrgency.Critical ? 1 : 0;
const bU = b.urgency === NotificationUrgency.Critical ? 1 : 0;
if (aU !== bU)
return bU - aU;
return b.time - a.time;
});
// Trim excess popups
const max = M.Modules.notifications.maxPopups || 4;
@ -174,6 +180,13 @@ QtObject {
root._byId[item.id] = item;
root.list.push(item);
}
root.list.sort((a, b) => {
const aU = a.urgency === NotificationUrgency.Critical ? 1 : 0;
const bU = b.urgency === NotificationUrgency.Critical ? 1 : 0;
if (aU !== bU)
return bU - aU;
return b.time - a.time;
});
root._changed();
} catch (e) {}
}

View file

@ -152,6 +152,11 @@ in
default = 15;
description = "Battery percentage for critical notification and blink.";
};
poweralertd = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable poweralertd for battery/power event notifications. Set to false to disable.";
};
};
weather = moduleOpt "weather" (
(intervalOpt 3600000)
@ -234,6 +239,10 @@ in
}
];
services.poweralertd.enable = lib.mkIf (
cfg.modules.battery.enable && cfg.modules.battery.poweralertd
) (lib.mkDefault true);
systemd.user.services.nova-shell = lib.mkIf cfg.systemd.enable {
Unit = {
Description = "nova-shell Quickshell bar";