Исходный код EJIO.ctk_extender.event.controller
## /ctk_extender/event/controller.py
import customtkinter as ctk
from collections.abc import Callable
from threading import Lock
from typing import Any, cast, NamedTuple
from loguru import logger
from EJIO.utils import StaticClass, Queue
from .base import Event
__all__ = [
'EventController',
]
class _ListenerInfo(NamedTuple):
"""Внутренняя структура для хранения метаданных подписчика."""
callback: Callable[[Any], None]
sender: Any | None
predicate: Callable[[Any], bool] | None
[документация]
class EventController(StaticClass):
"""
Потокобезопасный контроллер событий.
Поддерживает строгую фильтрацию событий по отправителю (sender) или кастомному предикату.
Вызывает обработчики строго в главном UI-потоке customtkinter.
"""
__slots__ = ()
_lock: Lock = Lock()
_queue: Queue[Event] = Queue()
_listeners: dict[type[Event], list[_ListenerInfo]] = {}
_root_widget: ctk.CTk | None = None
_polling_interval_ms: int = 16
[документация]
@classmethod
def initialize(cls, root_widget: ctk.CTk, polling_interval_ms: int = 16) -> None:
"""
Инициализирует контроллер и запускает бесконечный цикл опроса очереди.
:param root_widget: Корневой виджет ctk приложения.
:param polling_interval_ms: Интервал между обработкой событий.
"""
with cls._lock:
cls._root_widget = root_widget
cls._polling_interval_ms = polling_interval_ms
cls._root_widget.after(cls._polling_interval_ms, cls._process_queue)
logger.info("EventController for ctk_extender successfully initialized.")
[документация]
@classmethod
def subscribe[E: Event](
cls,
event_type: type[E],
callback: Callable[[E], None],
*,
sender: Any | None = None,
predicate: Callable[[E], bool] | None = None
) -> None:
"""
Подписывает обработчик на определенный класс события с возможностью фильтрации.
:param event_type: Класс события (наследник Event).
:param callback: Функция обратного вызова, принимающая экземпляр этого события.
:param sender: Опционально: фильтровать события строго от этого объекта-отправителя.
:param predicate: Опционально: кастомная функция-фильтр, возвращающая True/False.
"""
with cls._lock:
if event_type not in cls._listeners:
cls._listeners[event_type] = []
info = _ListenerInfo(
callback=cast(Callable[[Any], None], callback),
sender=sender,
predicate=cast(Callable[[Any], bool], predicate)
)
cls._listeners[event_type].append(info)
logger.debug(f"Subscriber added on {event_type.__name__} (Filter at sender: {sender is not None})")
[документация]
@classmethod
def unsubscribe[E: Event](cls, event_type: type[E], callback: Callable[[E], None]) -> None:
"""
Отписывает обработчик от указанного класса события.
:param event_type: Класс события (наследник Event).
:param callback: Функция обратного вызова, привязанная к этому событию.
"""
with cls._lock:
if event_type in cls._listeners:
# Удаляем все записи, связанные с этим коллбэком
cls._listeners[event_type] = [
info for info in cls._listeners[event_type] if info.callback != callback
]
[документация]
@classmethod
def emit(cls, event: Event) -> None:
"""
Потокобезопасно отправляет объект события в очередь.
:param event: Событие которое нужно отправить в очередь.
"""
cls._queue.push(event)
@classmethod
def _process_queue(cls) -> None:
"""Внутренний воркер опроса очереди. Выполняется в главном потоке."""
if cls._root_widget is None:
return
while not cls._queue.empty:
try:
event = cls._queue.pop()
event_type = type(event)
with cls._lock:
listeners = cls._listeners.get(event_type, []).copy()
for info in listeners:
try:
# --- Шаг фильтрации 1: Проверка по конкретному отправителю ---
if info.sender is not None and event.sender is not info.sender:
continue
# --- Шаг фильтрации 2: Проверка по кастомному предикату ---
if info.predicate is not None and not info.predicate(event):
continue
# Если все фильтры пройдены — вызываем обработчик
info.callback(event)
except Exception as e:
logger.exception(f"Critical error in event handler {event_type.__name__}: {e}")
# Явно уничтожаем локальную ссылку на событие, освобождая память в текущем такте
del event
except IndexError:
break
cls._root_widget.after(cls._polling_interval_ms, cls._process_queue)