Исходный код EJIO.ctk_extender.app.base

## /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 popup_info(self, title: str, message: str) -> None: """Показывает модальное информационное окно.""" if not self._is_running: logger.critical("Cannot execute command, application has not been started yet.") sys.exit(1) popup_info(title, message)
[документация] def popup_warning(self, title: str, message: str) -> None: """Показывает модальное предупреждение об ошибке.""" if not self._is_running: logger.critical("Cannot execute command, application has not been started yet.") sys.exit(1) popup_warning(title, message)
[документация] def popup_confirm(self, title: str, message: str, on_callback: Callable[[bool], None]) -> None: """Показывает модальный диалог подтверждения операции (Да/Нет) с защитой от крестика.""" if not self._is_running: logger.critical("Cannot execute command, application has not been started yet.") sys.exit(1) popup_confirm(title, message, on_callback)
[документация] def popup_image(self, title: str, source: str | Path | bytes) -> None: """Открывает независимое неблокирующее окно для просмотра картинок или байтовых графов.""" if not self._is_running: logger.critical("Cannot execute command, application has not been started yet.") sys.exit(1) popup_image(title, source)
# ===================================================================== # ФАСАД УВЕДОМЛЕНИЙ # =====================================================================
[документация] 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()