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.", "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.", "Splash-Screen auf das eingebettete CCMA-Hintergrundmotiv umgestellt und redundante Titeltexte entfernt.",
"Konfigurierbare Mindestanzeigezeit des Splash-Screens mit fünf Sekunden Standardwert ergänzt.", "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.", "Hausmeister um konfigurierbare Geburtstags- und Mitgliedsjubiläumsmeldungen erweitert.",
"Statusänderungen werden mit altem und neuem Klartextwert in der Mitgliederchronik protokolliert.", "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.", "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 dataclasses import dataclass
from pathlib import Path from pathlib import Path
from queue import Empty, Queue from queue import Empty, Queue
from tkinter import ttk
from ccma import __version__ from ccma import __version__
from ccma.domain.models import HousekeeperFinding 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 max(0.0, float(minimum_display_seconds)) if math.isfinite(float(minimum_display_seconds)) else 5.0
) )
self.shown_at = time.monotonic() self.shown_at = time.monotonic()
self.startup_finished = False
self._progress_job: str | None = None
self._messages: Queue[tuple[str, object]] = Queue() self._messages: Queue[tuple[str, object]] = Queue()
self.overrideredirect(True) self.overrideredirect(True)
self.resizable(False, False) self.resizable(False, False)
@@ -56,6 +57,7 @@ class SplashScreen(tk.Toplevel):
self.status_item: int self.status_item: int
self._build_ui() self._build_ui()
self._center() self._center()
self._update_progress()
self.after(120, self._start) self.after(120, self._start)
def _build_ui(self) -> None: def _build_ui(self) -> None:
@@ -88,8 +90,8 @@ class SplashScreen(tk.Toplevel):
fill="#e8edf2", fill="#e8edf2",
font=("TkFixedFont", 10), font=("TkFixedFont", 10),
) )
self.progress = ttk.Progressbar(self, mode="indeterminate") self.progress = SplashProgressBar(self, width=708, height=16)
self.canvas.create_window(30, 476, anchor="nw", width=708, window=self.progress) self.canvas.create_window(30, 476, anchor="nw", width=708, height=16, window=self.progress)
def _center(self) -> None: def _center(self) -> None:
self.update_idletasks() self.update_idletasks()
@@ -109,8 +111,6 @@ class SplashScreen(tk.Toplevel):
self.geometry(f"{width}x{height}+{x}+{y}") self.geometry(f"{width}x{height}+{x}+{y}")
def _start(self) -> None: def _start(self) -> None:
self.progress.start(10)
def worker() -> None: def worker() -> None:
try: try:
self._messages.put(("status", "Öffne Mitglieder-Store …")) self._messages.put(("status", "Öffne Mitglieder-Store …"))
@@ -150,7 +150,8 @@ class SplashScreen(tk.Toplevel):
self.after(30, self._poll_messages) self.after(30, self._poll_messages)
def _finish(self, result: StartupResult) -> None: 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._set_status(f"Bereit · {len(result.findings)} Vorgänge benötigen Aufmerksamkeit")
self._after_minimum(lambda: self._complete(result)) self._after_minimum(lambda: self._complete(result))
@@ -160,24 +161,78 @@ class SplashScreen(tk.Toplevel):
self.minimum_display_seconds, self.minimum_display_seconds,
time.monotonic(), 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: def _set_status(self, text: str) -> None:
self.canvas.itemconfigure(self.status_item, text=text) self.canvas.itemconfigure(self.status_item, text=text)
def _complete(self, result: StartupResult) -> None: def _complete(self, result: StartupResult) -> None:
self._cancel_progress_job()
self.destroy() self.destroy()
self.on_complete(result) self.on_complete(result)
def _fail(self, error: Exception) -> None: def _fail(self, error: Exception) -> None:
self.progress.stop() self.startup_finished = True
self._update_progress()
self._set_status("Start fehlgeschlagen") self._set_status("Start fehlgeschlagen")
self._after_minimum(lambda: self._complete_error(error)) self._after_minimum(lambda: self._complete_error(error))
def _complete_error(self, error: Exception) -> None: def _complete_error(self, error: Exception) -> None:
self._cancel_progress_job()
self.destroy() self.destroy()
self.on_error(error) 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( def centered_position(
*, *,
@@ -201,3 +256,12 @@ def centered_position(
def _remaining_minimum_ms(started_at: float, minimum_seconds: float, now: float) -> int: 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)) remaining = max(0.0, minimum_seconds - max(0.0, now - started_at))
return int(remaining * 1000 + 0.999) 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
+5 -1
View File
@@ -32,11 +32,15 @@ def test_splash_position_centers_on_pointer_and_stays_on_screen() -> None:
def test_splash_minimum_time_only_waits_for_remaining_duration() -> None: def test_splash_minimum_time_only_waits_for_remaining_duration() -> None:
from ccma.ui.splash import _remaining_minimum_ms from ccma.ui.splash import _progress_value, _remaining_minimum_ms
assert _remaining_minimum_ms(100.0, 5.0, 102.25) == 2750 assert _remaining_minimum_ms(100.0, 5.0, 102.25) == 2750
assert _remaining_minimum_ms(100.0, 5.0, 106.0) == 0 assert _remaining_minimum_ms(100.0, 5.0, 106.0) == 0
assert _remaining_minimum_ms(100.0, 0.0, 100.0) == 0 assert _remaining_minimum_ms(100.0, 0.0, 100.0) == 0
assert _progress_value(2.5, 5.0, startup_finished=False) == 47.5
assert _progress_value(5.0, 5.0, startup_finished=False) == 95.0
assert _progress_value(2.5, 5.0, startup_finished=True) == 50.0
assert _progress_value(5.0, 5.0, startup_finished=True) == 100.0
def test_event_labels_hide_board_actor_but_keep_automatic_marker() -> None: def test_event_labels_hide_board_actor_but_keep_automatic_marker() -> None: