## /ctk_extender/app/base.py
import sys
import customtkinter as ctk
from threading import Lock
from collections.abc import Callable
from typing import Any, Literal
from pathlib import Path
from loguru import logger
from EJIO.utils import Singleton, Localization, _
from ..config import AppSettings
from ..event import Event, EventController
from ..window import Window, WindowController
from ..windows import popup_warning, popup_confirm, popup_info, popup_image
from ..notification import BaseNotification, InAppNotificationController, DesktopNotificationController, StyleFlags
__all__ = [
'Application',
]
[документация]
class Application(Singleton):
"""
Глобальный оркестратор и единый фасадный интерфейс фреймворка ctk_extender.
Предоставляет методы для управления событиями, навигацией и жизненным циклом
окон без прямого обращения к внутренним контроллерам.
"""
def __init__(self, main_window_name: str) -> None:
"""
:param main_window_name: Фиксированный аргумент: Имя главного окна из реестра.
"""
self._lock: Lock = Lock()
self._main_window_name: str = main_window_name
self._is_running: bool = False
self._is_closing: bool = False
self.context_root: ctk.CTk | None = None
@property
def is_running(self) -> bool:
return self._is_running
# =====================================================================
# ФАСАДНЫЕ МЕТОДЫ УПРАВЛЕНИЯ СОБЫТИЯМИ
# =====================================================================
[документация]
def emit(self, event: Event) -> None:
"""Потокобезопасно отправляет событие в глобальную шину."""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
EventController.emit(event)
[документация]
def subscribe[E: Event](
self,
event_type: type[E],
callback: Callable[[E], None],
*,
sender: Any | None = None,
predicate: Callable[[E], bool] | None = None
) -> None:
"""Подписывает обработчик на определенное событие с фильтрацией."""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
EventController.subscribe(event_type, callback, sender=sender, predicate=predicate)
[документация]
def unsubscribe[E: Event](self, event_type: type[E], callback: Callable[[E], None]) -> None:
"""Отписывает обработчик от события."""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
EventController.unsubscribe(event_type, callback)
# =====================================================================
# ФАСАДНЫЕ МЕТОДЫ УПРАВЛЕНИЯ ОКНАМИ И НАВИГАЦИЕЙ
# =====================================================================
[документация]
def open_window(self, name: str, *args: Any, **kwargs: Any) -> Window:
"""Создает и выводит на экран окно из WindowRegistry."""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
return WindowController.open_window(name, *args, **kwargs)
[документация]
def close_window(self, name: str) -> None:
"""Закрывает окно по его имени."""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
WindowController.close_window(name)
[документация]
def get_window(self, name: str) -> Window:
"""Возвращает живой объект активного окна по имени."""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
return WindowController.get_active(name)
[документация]
def navigate_to(self, window_name: str, layout_name: str) -> None:
"""Переключает текущий макет указанного окна (с полной очисткой стека)."""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
WindowController.get_active(window_name).switch_layout(layout_name)
[документация]
def push_screen(self, window_name: str, layout_name: str) -> None:
"""Помещает новый экран на вершину стека навигации указанного окна."""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
WindowController.get_active(window_name).push_layout(layout_name)
[документация]
def pop_screen(self, window_name: str) -> None:
"""Возвращает указанное окно на предыдущий экран навигационного стека."""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
WindowController.get_active(window_name).pop_layout()
# =====================================================================
# ФАСАД ЛОКАЛИЗАЦИИ И ИНТЕРНАЦИОНАЛИЗАЦИИ (i18n Interface)
# =====================================================================
[документация]
def translate(self, key_path: str, default: str | None = None) -> str:
"""
Извлекает локализованную строку по её точечному пути.
Дублирует поведение глобальной функции _().
"""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
return Localization.get(key_path, default)
[документация]
def change_language(self, locale: str) -> None:
"""Переключает текущий язык интерфейса приложения в рантайме."""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
Localization.set_locale(locale)
# =====================================================================
# ФАСАД ВСПЛЫВАЮЩИХ ОКН И ДИАЛОГОВ
# =====================================================================
# =====================================================================
# ФАСАД УВЕДОМЛЕНИЙ
# =====================================================================
[документация]
def notify(
self,
notification_cls: type[BaseNotification],
target: Literal["in_app", "desktop"] = "in_app",
flags: StyleFlags = StyleFlags.DEFAULT,
position: str = "top_right",
duration: float = 3.5,
**kwargs: Any
) -> None:
"""
Единая полиморфная точка входа для отправки уведомлений во фреймворке.
Прозрачно проксирует переданный ООП-класс уведомления в контроллеры.
"""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
_controller = DesktopNotificationController if target == "desktop" else InAppNotificationController
_controller.push(
notification_cls,
flags=flags,
position=position,
duration=duration,
**kwargs
)
# =====================================================================
# СТАРТ И ЗАВЕРШЕНИЕ РАНТАЙМА
# =====================================================================
[документация]
def run(self) -> None:
"""Запускает приложение, инициализируя тему, локали и главный цикл Tkinter."""
with self._lock:
if self._is_running:
return
if self._is_closing:
return
self._is_running = True
# Считываем константы из структуры AppSettings
ctk.set_appearance_mode(AppSettings.appearance_mode)
ctk.set_default_color_theme(AppSettings.color_theme)
Localization.initialize()
try:
# Открываем главное окно
main_window = WindowController.open_window(self._main_window_name)
self.context_root = main_window.ctk_instance
# Активируем шину событий
EventController.initialize(main_window.ctk_instance)
# Просим Tcl/Tk обработать все фоновые геометрические задачи разметки
main_window.ctk_instance.update_idletasks()
# Принудительно заставляем ОС зарегистрировать дескриптор главного окна.
main_window.ctk_instance.update()
# Планируем вызов хука mount() через 50 миллисекунд.
main_window.ctk_instance.after(50, main_window.mount)
logger.info(f"Application is started. Running window: {self._main_window_name}")
main_window.ctk_instance.mainloop()
except Exception as e:
logger.critical(f"Critical Application runtime error: {e}")
self.exit(exit_code=1)
finally:
self.exit(exit_code=0)
[документация]
def exit(self, exit_code: int = 0) -> None:
"""Полная каскадная выгрузка всех компонентов и завершение работы."""
if not self._is_running:
logger.critical("Cannot execute command, application has not been started yet.")
sys.exit(1)
with self._lock:
self._is_running = False
self._is_closing = True
logger.info(f"The application shutdown procedure with code: {exit_code} has been started")
try:
WindowController.clear()
except Exception as e:
logger.error(f"Error clearing windows at exit: {e}")
logger.info("Application completely stopped. Exiting process runtime.")
sys.exit(exit_code)
def __del__(self) -> None:
self.exit()