feat: style timed splash progress

This commit is contained in:
Marcel Peterkau
2026-06-21 18:51:31 +02:00
parent e1d2b87ca1
commit fc042f6711
3 changed files with 78 additions and 9 deletions
+1
View File
@@ -28,6 +28,7 @@
"Mehrstufiger Mahnworkflow mit Hausmeister-Regel, Entwurf, Versandbestätigung, Zahlungsfrist, optionaler Gebühr und Mahnsperre ergänzt.",
"Splash-Screen auf das eingebettete CCMA-Hintergrundmotiv umgestellt und redundante Titeltexte entfernt.",
"Konfigurierbare Mindestanzeigezeit des Splash-Screens mit fünf Sekunden Standardwert ergänzt.",
"Theme-unabhängigen Splash-Fortschrittsbalken in Bild-Blau mit Silberrahmen und zeitbasiertem Fortschritt eingeführt.",
"Hausmeister um konfigurierbare Geburtstags- und Mitgliedsjubiläumsmeldungen erweitert.",
"Statusänderungen werden mit altem und neuem Klartextwert in der Mitgliederchronik protokolliert.",
"Fensterposition, normaler Fensterzustand und Maximierung werden gespeichert; der Splash startet auf dem zuletzt verwendeten Monitor.",
+72 -8
View File
@@ -8,7 +8,6 @@ from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from queue import Empty, Queue
from tkinter import ttk
from ccma import __version__
from ccma.domain.models import HousekeeperFinding
@@ -48,6 +47,8 @@ class SplashScreen(tk.Toplevel):
max(0.0, float(minimum_display_seconds)) if math.isfinite(float(minimum_display_seconds)) else 5.0
)
self.shown_at = time.monotonic()
self.startup_finished = False
self._progress_job: str | None = None
self._messages: Queue[tuple[str, object]] = Queue()
self.overrideredirect(True)
self.resizable(False, False)
@@ -56,6 +57,7 @@ class SplashScreen(tk.Toplevel):
self.status_item: int
self._build_ui()
self._center()
self._update_progress()
self.after(120, self._start)
def _build_ui(self) -> None:
@@ -88,8 +90,8 @@ class SplashScreen(tk.Toplevel):
fill="#e8edf2",
font=("TkFixedFont", 10),
)
self.progress = ttk.Progressbar(self, mode="indeterminate")
self.canvas.create_window(30, 476, anchor="nw", width=708, window=self.progress)
self.progress = SplashProgressBar(self, width=708, height=16)
self.canvas.create_window(30, 476, anchor="nw", width=708, height=16, window=self.progress)
def _center(self) -> None:
self.update_idletasks()
@@ -109,8 +111,6 @@ class SplashScreen(tk.Toplevel):
self.geometry(f"{width}x{height}+{x}+{y}")
def _start(self) -> None:
self.progress.start(10)
def worker() -> None:
try:
self._messages.put(("status", "Öffne Mitglieder-Store …"))
@@ -150,7 +150,8 @@ class SplashScreen(tk.Toplevel):
self.after(30, self._poll_messages)
def _finish(self, result: StartupResult) -> None:
self.progress.stop()
self.startup_finished = True
self._update_progress()
self._set_status(f"Bereit · {len(result.findings)} Vorgänge benötigen Aufmerksamkeit")
self._after_minimum(lambda: self._complete(result))
@@ -160,24 +161,78 @@ class SplashScreen(tk.Toplevel):
self.minimum_display_seconds,
time.monotonic(),
)
self.after(remaining_ms, callback)
self.after(remaining_ms, lambda: self._show_completed_progress(callback))
def _show_completed_progress(self, callback: Callable[[], None]) -> None:
self.startup_finished = True
self.progress.set(100)
self.after_idle(callback)
def _set_status(self, text: str) -> None:
self.canvas.itemconfigure(self.status_item, text=text)
def _complete(self, result: StartupResult) -> None:
self._cancel_progress_job()
self.destroy()
self.on_complete(result)
def _fail(self, error: Exception) -> None:
self.progress.stop()
self.startup_finished = True
self._update_progress()
self._set_status("Start fehlgeschlagen")
self._after_minimum(lambda: self._complete_error(error))
def _complete_error(self, error: Exception) -> None:
self._cancel_progress_job()
self.destroy()
self.on_error(error)
def _update_progress(self) -> None:
if not self.winfo_exists():
return
elapsed = max(0.0, time.monotonic() - self.shown_at)
value = _progress_value(elapsed, self.minimum_display_seconds, self.startup_finished)
self.progress.set(value)
if value < 100 or not self.startup_finished:
self._cancel_progress_job()
self._progress_job = self.after(40, self._update_progress)
def _cancel_progress_job(self) -> None:
if not self._progress_job:
return
try:
self.after_cancel(self._progress_job)
except tk.TclError:
pass
self._progress_job = None
class SplashProgressBar(tk.Canvas):
TRACK_COLOR = "#071a29"
FILL_COLOR = "#2389c9"
BORDER_COLOR = "#aeb8c2"
def __init__(self, master: tk.Misc, *, width: int, height: int):
inner_width = max(1, width - 4)
inner_height = max(1, height - 4)
super().__init__(
master,
width=inner_width,
height=inner_height,
background=self.TRACK_COLOR,
borderwidth=0,
highlightthickness=2,
highlightbackground=self.BORDER_COLOR,
highlightcolor=self.BORDER_COLOR,
)
self._bar_width = inner_width
self._bar_height = inner_height
self.fill_item = self.create_rectangle(0, 0, 0, inner_height, fill=self.FILL_COLOR, outline="")
def set(self, percent: float) -> None:
value = min(100.0, max(0.0, float(percent)))
self.coords(self.fill_item, 0, 0, self._bar_width * value / 100.0, self._bar_height)
def centered_position(
*,
@@ -201,3 +256,12 @@ def centered_position(
def _remaining_minimum_ms(started_at: float, minimum_seconds: float, now: float) -> int:
remaining = max(0.0, minimum_seconds - max(0.0, now - started_at))
return int(remaining * 1000 + 0.999)
def _progress_value(elapsed: float, minimum_seconds: float, startup_finished: bool) -> float:
if minimum_seconds <= 0:
return 100.0 if startup_finished else 5.0
elapsed_ratio = min(1.0, max(0.0, elapsed) / minimum_seconds)
if startup_finished:
return elapsed_ratio * 100.0
return elapsed_ratio * 95.0