mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 03:04: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.",
|
"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
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user