Исходный код EJIO.ctk_extender.notification.in_app_controller
## /ctk_extender/notification/in_app_controller.py
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__ = ['InAppNotificationController']
[документация]
class InAppNotificationController(StaticClass, Disposable):
"""
Статический контроллер внешних ООП-уведомлений главного окна.
Использует потокобезопасную Queue буферизации для FIFO-ротации.
"""
_LOCK: Lock = Lock()
_max_visible: int = 3
_active_widgets: list[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 = 3.5,
**kwargs: Any
) -> None:
"""
Потокобезопасно пушит ООП-карточку на главное окно или в Queue буфер.
:param notification_cls: Класс уведомления.
:param flags: Флаги стиля уведомления.
:param position: Позиция уведомления.
:param duration: Длительность показа уведомления в с.
:param kwargs: Дополнительные параметры инициализации класса уведомления.
"""
with cls._LOCK:
if len(cls._active_widgets) < 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: Дополнительные параметры инициализации класса уведомления.
"""
from ..app.base import Application
from ..window import WindowController
app = Application()
root = app.context_root
if not root:
logger.warning(f"No root window found for notification {cls_type.__name__}.")
return
try:
active_win = WindowController.get_active(app._main_window_name)
notification_parent = active_win.active_layout_canvas
except Exception:
notification_parent = root
if "width" not in kwargs:
kwargs["width"] = 240
if "height" not in kwargs:
kwargs["height"] = 46
# Создаем пустой фон на базе флагов
node = cls_type(notification_parent, flags=flags, **kwargs)
cls._active_widgets.append(node)
try:
node.ctk_instance.lift()
notification_parent.update_idletasks()
except Exception:
logger.error(f"Lifting in-app notification {cls_type.__name__} failed.")
# Обновляем геометрию
pos_strategy = get_position_strategy(pos_name)
cls._reposition(pos_strategy)
logger.debug(f"Showing in-app notification {cls_type.__name__}.")
notification_parent.after(int(dur * 1000), lambda: cls._expire(node, pos_strategy))
@classmethod
def _reposition(cls, pos_strategy: Any) -> None:
"""Пересчитывает абсолютные экранные координаты стопки уведомлений."""
gap = 8
for idx, widget in enumerate(reversed(cls._active_widgets)):
try:
offset_y = 15 + idx * (widget.height + gap)
pos_strategy.apply_in_app(widget.ctk_instance, offset_y)
except Exception:
continue
@classmethod
def _expire(cls, widget: BaseNotification, pos_strategy: Any) -> None:
"""Уничтожает отгоревшее уведомление и вытягивает следующее из очереди."""
with cls._LOCK:
if widget in cls._active_widgets:
cls._active_widgets.remove(widget)
cls._reposition(pos_strategy)
try:
widget.destroy()
except Exception:
pass
if not cls._queue.empty:
next_node = cls._queue.pop()
cls_type, flags, pos_name, dur, kwargs = next_node
cls._display(cls_type, flags, pos_name, dur, kwargs)
logger.debug(f"Closing in-app notification {widget.__class__.__name__}")
[документация]
@classmethod
def clear(cls) -> None:
"""Полная выгрузка всех внешних окон при экстренном закрытии приложения."""
with cls._LOCK:
for w in list(cls._active_widgets):
try:
w.destroy()
except Exception:
pass
cls._active_widgets.clear()
while not cls._queue.empty: cls._queue.pop()