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