From 6a678c879e91597daf91fc1f82b2d9543877c6d8 Mon Sep 17 00:00:00 2001 From: Karel Capek Robotics <96583804+MelkorBalrog@users.noreply.github.com> Date: Wed, 8 Oct 2025 10:28:28 -0400 Subject: [PATCH] Fix splash screen animation cleanup --- HISTORY.md | 7 +++ gui/windows/splash_screen.py | 10 ++++ .../test_splash_screen_cleanup.py | 55 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tests/detachment/after_callbacks/test_splash_screen_cleanup.py diff --git a/HISTORY.md b/HISTORY.md index cb1e8b5e..99c739e0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -18,6 +18,13 @@ # along with this program. If not, see . --> +## 0.2.280 - 2025-09-16 + +- Cancel splash screen animation and fade timers before destruction to prevent + ``invalid command name *_animate`` errors during shutdown. +- Add regression tests confirming splash screen cleanup removes ``_animate`` + callbacks from Tk's scheduler and Tcl command registry. + ## 0.2.279 - 2025-09-15 - Track dockable diagram notebook ownership to synchronise floating state and diff --git a/gui/windows/splash_screen.py b/gui/windows/splash_screen.py index d8d52a9c..36ed4cad 100644 --- a/gui/windows/splash_screen.py +++ b/gui/windows/splash_screen.py @@ -21,6 +21,8 @@ import random from PIL import Image, ImageDraw, ImageTk, ImageFont +from gui.utils.tk_utils import cancel_after_events + class SplashScreen(tk.Toplevel): """Simple splash screen with rotating cube and gear.""" @@ -39,6 +41,7 @@ def __init__( self.duration = duration self.overrideredirect(True) self._on_close = on_close + self._closed = False # Track whether transparency is supported try: @@ -489,6 +492,13 @@ def _animate(self): def _close(self): """Destroy splash screen and accompanying shadow window.""" + if self._closed: + return + self._closed = True + cancel_after_events(self) + shadow = getattr(self, "shadow", None) + if isinstance(shadow, tk.Misc): + cancel_after_events(shadow) try: self.shadow.destroy() except Exception: diff --git a/tests/detachment/after_callbacks/test_splash_screen_cleanup.py b/tests/detachment/after_callbacks/test_splash_screen_cleanup.py new file mode 100644 index 00000000..0e6d0df7 --- /dev/null +++ b/tests/detachment/after_callbacks/test_splash_screen_cleanup.py @@ -0,0 +1,55 @@ +# Author: Miguel Marina +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Copyright (C) 2025 Capek System Safety & Robotic Solutions +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Regression tests for splash screen callback cleanup.""" + +from __future__ import annotations + +import os +import tkinter as tk + +import pytest + +from gui.windows.splash_screen import SplashScreen + + +@pytest.mark.skipif("DISPLAY" not in os.environ, reason="Tk display not available") +class TestSplashScreenCleanup: + """Grouped tests ensuring splash screen timers are cancelled on close.""" + + def test_close_cancels_animation_callbacks(self) -> None: + root = tk.Tk() + root.withdraw() + try: + splash = SplashScreen(root, duration=0, on_close=lambda: None) + root.update_idletasks() + splash._close() + root.update() + scheduled = root.tk.call("after", "info") + if isinstance(scheduled, str): + scheduled = (scheduled,) + scheduled_repr = " ".join(map(str, scheduled)) + assert "_animate" not in scheduled_repr + commands = getattr(root, "_tclCommands", {}) + joined = " ".join(map(str, commands)) + assert "_animate" not in joined + finally: + try: + root.destroy() + except Exception: + pass