1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-04 04:42:05 -04:00
Files
DankMaterialShell/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml

240 lines
7.6 KiB
QML

import QtQuick
import qs.Common
import qs.Services
QtObject {
id: manager
property var modelData
property int topMargin: 0
readonly property bool compactMode: SettingsData.notificationCompactMode
readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding
readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal
readonly property real actionButtonHeight: compactMode ? 20 : 24
readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS
readonly property real popupSpacing: compactMode ? 0 : Theme.spacingXS
readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing
property var popupWindows: []
property var destroyingWindows: new Set()
property var pendingDestroys: []
property int destroyDelayMs: 100
property Component popupComponent
popupComponent: Component {
NotificationPopup {
onExitFinished: manager._onPopupExitFinished(this)
onPopupHeightChanged: manager._onPopupHeightChanged(this)
}
}
property Connections notificationConnections
notificationConnections: Connections {
function onVisibleNotificationsChanged() {
manager._sync(NotificationService.visibleNotifications);
}
target: NotificationService
}
property Timer sweeper
property Timer destroyTimer: Timer {
interval: destroyDelayMs
running: false
repeat: false
onTriggered: manager._processDestroyQueue()
}
function _processDestroyQueue() {
if (pendingDestroys.length === 0)
return;
const p = pendingDestroys.shift();
if (p && p.destroy) {
try {
p.destroy();
} catch (e) {}
}
if (pendingDestroys.length > 0)
destroyTimer.restart();
}
function _scheduleDestroy(p) {
if (!p)
return;
pendingDestroys.push(p);
if (!destroyTimer.running)
destroyTimer.restart();
}
sweeper: Timer {
interval: 500
running: false
repeat: true
onTriggered: {
const toRemove = [];
for (const p of popupWindows) {
if (!p) {
toRemove.push(p);
continue;
}
const isZombie = p.status === Component.Null || (!p.visible && !p.exiting) || (!p.notificationData && !p._isDestroying) || (!p.hasValidData && !p._isDestroying);
if (isZombie) {
toRemove.push(p);
if (p.forceExit) {
p.forceExit();
} else if (p.destroy) {
try {
p.destroy();
} catch (e) {}
}
}
}
if (toRemove.length) {
popupWindows = popupWindows.filter(p => toRemove.indexOf(p) === -1);
_repositionAll();
}
if (popupWindows.length === 0)
sweeper.stop();
}
}
function _hasWindowFor(w) {
return popupWindows.some(p => p && p.notificationData === w && !p._isDestroying && p.status !== Component.Null);
}
function _isValidWindow(p) {
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
}
function _isFocusedScreen() {
if (!SettingsData.notificationFocusedMonitor)
return true;
const focused = CompositorService.getFocusedScreen();
return focused && manager.modelData && focused.name === manager.modelData.name;
}
function _sync(newWrappers) {
for (const p of popupWindows.slice()) {
if (!_isValidWindow(p) || p.exiting)
continue;
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1) {
p.notificationData.removedByLimit = true;
p.notificationData.popup = false;
}
}
for (const w of newWrappers) {
if (w && !_hasWindowFor(w) && _isFocusedScreen())
_insertAtTop(w);
}
}
function _popupHeight(p) {
return (p.alignedHeight || p.implicitHeight || (baseNotificationHeight - popupSpacing)) + popupSpacing;
}
function _insertAtTop(wrapper) {
if (!wrapper)
return;
const notificationId = wrapper?.notification ? wrapper.notification.id : "";
const win = popupComponent.createObject(null, {
"notificationData": wrapper,
"notificationId": notificationId,
"screenY": topMargin,
"screen": manager.modelData
});
if (!win)
return;
if (!win.hasValidData) {
win.destroy();
return;
}
popupWindows.unshift(win);
_repositionAll();
if (!sweeper.running)
sweeper.start();
}
function _repositionAll() {
const active = popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting);
const pinnedSlots = [];
for (const p of active) {
if (!p.hovered)
continue;
pinnedSlots.push({
y: p.screenY,
end: p.screenY + _popupHeight(p)
});
}
pinnedSlots.sort((a, b) => a.y - b.y);
let currentY = topMargin;
for (const win of active) {
if (win.hovered)
continue;
for (const slot of pinnedSlots) {
if (currentY >= slot.y - 1 && currentY < slot.end)
currentY = slot.end;
}
win.screenY = currentY;
currentY += _popupHeight(win);
}
}
function _onPopupHeightChanged(p) {
if (!p || p.exiting || p._isDestroying)
return;
if (popupWindows.indexOf(p) === -1)
return;
_repositionAll();
}
function _onPopupExitFinished(p) {
if (!p)
return;
const windowId = p.toString();
if (destroyingWindows.has(windowId))
return;
destroyingWindows.add(windowId);
const i = popupWindows.indexOf(p);
if (i !== -1) {
popupWindows.splice(i, 1);
popupWindows = popupWindows.slice();
}
if (NotificationService.releaseWrapper && p.notificationData)
NotificationService.releaseWrapper(p.notificationData);
_scheduleDestroy(p);
Qt.callLater(() => destroyingWindows.delete(windowId));
_repositionAll();
}
function cleanupAllWindows() {
sweeper.stop();
destroyTimer.stop();
pendingDestroys = [];
for (const p of popupWindows.slice()) {
if (p) {
try {
if (p.forceExit) {
p.forceExit();
} else if (p.destroy) {
p.destroy();
}
} catch (e) {}
}
}
popupWindows = [];
destroyingWindows.clear();
}
onPopupWindowsChanged: {
if (popupWindows.length > 0 && !sweeper.running) {
sweeper.start();
} else if (popupWindows.length === 0 && sweeper.running) {
sweeper.stop();
}
}
}