from __future__ import annotations import tkinter as tk from pathlib import Path from tkinter import filedialog, messagebox from ccma import __version__ from ccma.config import load_config from ccma.domain.dates import setup_system_locale from ccma.storage.repository import MemberRepository from ccma.ui.main_window import MainWindow from ccma.ui.monitors import ( centered_geometry, ensure_visible_geometry, monitor_for_geometry, preferred_monitor, ) from ccma.ui.splash import SplashScreen, StartupResult from ccma.ui.theme import load_theme from ccma.ui.window_state import is_maximized, maximize class CCMAApp(tk.Tk): def __init__(self): super().__init__() setup_system_locale() self.config_obj = load_config() self.title(f"CCMA · v{__version__}") self.minsize(1050, 650) load_theme(self, self.config_obj.theme_mode) self.startup_monitor = preferred_monitor(self, self.config_obj.monitor_bounds) initial_geometry = self.config_obj.window_geometry or centered_geometry( 1500, 860, self.startup_monitor ) self.geometry(ensure_visible_geometry(initial_geometry, self.startup_monitor)) self._last_normal_geometry = self.geometry() self._geometry_capture_job: str | None = None self.bind("", self._remember_window_geometry) self.protocol("WM_DELETE_WINDOW", self.close) self.withdraw() self.after_idle(self._start) def _start(self) -> None: store_path = self._resolve_store() if not store_path: self.destroy() return repository = MemberRepository(store_path) try: SplashScreen( self, repository, self._startup_complete, self._startup_failed, run_housekeeper=self.config_obj.run_housekeeper_on_startup, monitor=self.startup_monitor, housekeeper_settings=self.config_obj.housekeeper_settings(), minimum_display_seconds=self.config_obj.splash_minimum_seconds, ) except Exception as exc: self._startup_failed(exc) def _resolve_store(self) -> Path | None: configured = self.config_obj.store_path.strip() if configured: return Path(configured).expanduser() selected = filedialog.askdirectory( parent=self, title="Zentrales CCMA-Mitgliederverzeichnis auswählen oder anlegen", initialdir=str(Path.home()), mustexist=False, ) if not selected: return None self.config_obj.store_path = selected self.config_obj.save() return Path(selected) def _startup_complete(self, result: StartupResult) -> None: self.deiconify() main = MainWindow( self, result.repository, self.config_obj, result.findings, result.validation_errors, ) main.pack(fill="both", expand=True) if self.config_obj.window_state in {"maximized", "zoomed"}: self.after_idle(lambda: maximize(self)) def _startup_failed(self, error: Exception) -> None: self.deiconify() messagebox.showerror("CCMA konnte nicht gestartet werden", str(error), parent=self) self.destroy() def _remember_window_geometry(self, _event: tk.Event | None = None) -> None: if self._geometry_capture_job: try: self.after_cancel(self._geometry_capture_job) except tk.TclError: pass self._geometry_capture_job = self.after(150, self._capture_normal_geometry) def _capture_normal_geometry(self) -> None: self._geometry_capture_job = None try: if self.state() == "normal" and not is_maximized(self) and self.winfo_viewable(): self._last_normal_geometry = self.winfo_geometry() except tk.TclError: return def close(self) -> None: try: maximized = is_maximized(self) current_geometry = self.winfo_geometry() if not maximized: self._last_normal_geometry = current_geometry self.config_obj.window_geometry = self._last_normal_geometry self.config_obj.window_state = "maximized" if maximized else "normal" monitor = monitor_for_geometry(self, current_geometry) self.config_obj.monitor_bounds = monitor.as_tuple() self.config_obj.save() except (OSError, tk.TclError): pass self.destroy() def main() -> None: app = CCMAApp() app.mainloop() if __name__ == "__main__": main()