mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 11:14:52 +02:00
feat: style timed splash progress
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
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, 106.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:
|
||||
|
||||
Reference in New Issue
Block a user