## /ctk_extender/notification/desktop_controller.py
import customtkinter as ctk
from threading import Lock
from loguru import logger
from typing import Any
from EJIO.utils import StaticClass, Queue, Disposable
from .base import BaseNotification
from .flags import StyleFlags
from .position import get_position_strategy
__all__ = [
'DesktopNotificationController',
]
[документация]
class DesktopNotificationController(StaticClass, Disposable):
"""
Статический контроллер внешних ООП-уведомлений рабочего стола.
Использует потокобезопасную Queue буферизации для FIFO-ротации.
"""
__slots__ = ()
_LOCK: Lock = Lock()
_max_visible: int = 3
_active_nodes: list[tuple[ctk.CTkToplevel, BaseNotification]] = []
_queue: Queue[tuple[type[BaseNotification], StyleFlags, str, float, dict[str, Any]]] = Queue()
[документация]
@classmethod
def push(
cls,
notification_cls: type[BaseNotification],
flags: StyleFlags = StyleFlags.DEFAULT,
position: str = "top_right",
duration: float = 4.0,
**kwargs: Any
) -> None:
"""
Потокобезопасно пушит ООП-карточку на рабочий стол ОС или в Queue буфер.
:param notification_cls: Класс уведомления.
:param flags: Флаги стиля уведомления.
:param position: Позиция уведомления.
:param duration: Длительность показа уведомления в с.
:param kwargs: Дополнительные параметры инициализации класса уведомления.
"""
with cls._LOCK:
if len(cls._active_nodes) < cls._max_visible:
cls._display(notification_cls, flags, position, duration, kwargs)
else:
cls._queue.push((notification_cls, flags, position, duration, kwargs))
@classmethod
def _display(
cls,
cls_type: type[BaseNotification],
flags: StyleFlags,
pos_name: str,
dur: float,
kwargs: dict[str, Any]
) -> None:
"""
Отображает уведомление на экране.
:param cls_type: Класс уведомления.
:param flags: Флаги стиля уведомления.
:param pos_name: Позиция уведомления.
:param dur: Длительность показа уведомления в с.
:param kwargs: Дополнительные параметры инициализации класса уведомления.
"""
# 1. Создаем внешнюю Си-раму Toplevel без системных границ
win = ctk.CTkToplevel()
win.overrideredirect(True)
win.attributes("-topmost", True)
win.withdraw() # Скрываем на время конструирования внутренних стилей
# 2. Инстанцируем переданный ООП-класс уведомления, отдавая win как parent
widget_instance = cls_type(win, flags=flags, **kwargs)
# Растягиваем внутренний фрейм виджета на весь размер Toplevel окна
widget_instance.ctk_instance.pack(fill="both", expand=True)
cls._active_nodes.append((win, widget_instance))
# Вычисляем абсолютные экранные координаты геометрии
pos_strategy = get_position_strategy(pos_name)
cls._reposition(pos_strategy)
win.deiconify()
logger.debug(f"Showing desktop notification {cls_type.__name__}")
# Планируем каскадное закрытие по таймеру
win.after(int(dur * 1000), lambda: cls._expire(win, widget_instance, pos_strategy))
@classmethod
def _reposition(cls, pos_strategy: Any) -> None:
"""Пересчитывает абсолютные экранные координаты стопки окон."""
gap = 10
for idx, (win, widget) in enumerate(reversed(cls._active_nodes)):
# Вычисляем сдвиг с учетом реальной кастомной высоты каждого переданного виджета
offset_y = 40 + idx * (widget.height + gap)
pos_strategy.apply_desktop(win, offset_y, widget.width, widget.height)
@classmethod
def _expire(cls, win: ctk.CTkToplevel, widget: BaseNotification, pos_strategy: Any) -> None:
"""Уничтожает отгоревшее окно и вытягивает следующее уведомление из очереди."""
with cls._LOCK:
for active_win, active_widget in list(cls._active_nodes):
if active_win == win:
cls._active_nodes.remove((active_win, active_widget))
try:
active_widget.destroy()
active_win.destroy()
except Exception:
pass
# Пересчитываем сдвиг для оставшихся окон на экране
cls._reposition(pos_strategy)
# Извлекаем отложенное уведомление из Queue буфера
if not cls._queue.empty:
next_notify = cls._queue.pop()
cls_type, flags, pos_name, dur, kwargs = next_notify
cls._display(cls_type, flags, pos_name, dur, kwargs)
logger.debug(f"Closing desktop notification {widget.__class__.__name__}")
break
[документация]
@classmethod
def clear(cls) -> None:
"""Полная выгрузка всех внешних окон при экстренном закрытии приложения."""
with cls._LOCK:
for win, widget in list(cls._active_nodes):
try:
widget.destroy()
win.destroy()
except Exception:
pass
cls._active_nodes.clear()
while not cls._queue.empty:
cls._queue.pop()