Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
-->

## 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
Expand Down
10 changes: 10 additions & 0 deletions gui/windows/splash_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
55 changes: 55 additions & 0 deletions tests/detachment/after_callbacks/test_splash_screen_cleanup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Author: Miguel Marina <karel.capek.robotics@gmail.com>
# 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 <https://www.gnu.org/licenses/>.

"""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