From eaf5796aefb6142f0d55a0a63f26c2dba20bcc38 Mon Sep 17 00:00:00 2001 From: Karel Capek Robotics <96583804+MelkorBalrog@users.noreply.github.com> Date: Thu, 4 Sep 2025 07:52:15 -0400 Subject: [PATCH] Cancel tab callbacks before widget cleanup --- HISTORY.md | 3 +++ README.md | 6 +++--- gui/utils/closable_notebook.py | 32 ++++++++++++++++++++++++++------ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index cff6c5b0..0d063620 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -19,6 +19,9 @@ --> # Version History +- 0.2.247 - Cancel tab callbacks before forgetting or destroying widgets during + moves and detachment to prevent orphaned Tcl commands such as + ``*_animate``. - 0.2.246 - Guard tab rollback by checking target ownership and swallowing race-condition errors when forgetting tabs. - 0.2.245 - Verify tab parentage after moves so detached tabs skip diff --git a/README.md b/README.md index 2c9e142f..af99953a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -version: 0.2.246 +version: 0.2.247 Author: Miguel Marina - [LinkedIn](https://www.linkedin.com/in/progman32/) -This release safeguards tab rollback by verifying target ownership before -forgetting a tab and swallowing race-condition Tcl errors. +This release cancels tab callbacks before widgets are forgotten or destroyed, +preventing orphaned Tcl commands during tab moves and detachment. # AutoML AutoML is an automotive modeling and analysis tool built around a SysML-based metamodel. It lets you describe items, operating scenarios, functions, structure and interfaces in a single environment. diff --git a/gui/utils/closable_notebook.py b/gui/utils/closable_notebook.py index d6c3be3d..e7d2b45d 100644 --- a/gui/utils/closable_notebook.py +++ b/gui/utils/closable_notebook.py @@ -466,15 +466,15 @@ def _handle_close(self, event: tk.Event) -> bool: child = self.nametowidget(tab_id) self._closing_tab = tab_id self.event_generate("<>") + try: + self._cancel_after_events(child) + except Exception: + pass if tab_id in self.tabs(): try: self.forget(tab_id) except tk.TclError: pass - try: - self._cancel_after_events(child) - except Exception: - pass try: child.destroy() except Exception: @@ -532,7 +532,9 @@ def _move_tab(self, tab_id: str, target: "ClosableNotebook") -> bool: The method first tries Tk's ``winfo`` reparenting to avoid cloning. If the operation fails, the widget is restored to its original notebook and - the caller may fall back to cloning. + the caller may fall back to cloning. Tk ``after`` callbacks are + cancelled before widgets are forgotten to avoid orphaned Tcl commands + such as ``*_animate``. """ text = self.tab(tab_id, "text") @@ -540,6 +542,10 @@ def _move_tab(self, tab_id: str, target: "ClosableNotebook") -> bool: def _safe_forget(nb: "ClosableNotebook", widget: tk.Widget) -> None: if widget.master is nb or str(widget) in nb.tabs(): + try: + self._cancel_after_events(widget) + except Exception: + pass try: nb.forget(widget) except tk.TclError: @@ -577,6 +583,10 @@ def _safe_forget(nb: "ClosableNotebook", widget: tk.Widget) -> None: self.add(child, text=text) self.select(child) if isinstance(self.master, tk.Toplevel) and not self.tabs(): + try: + self._cancel_after_events(self.master) + except Exception: + pass self.master.destroy() return moved @@ -1284,7 +1294,11 @@ def _copy_tree_item( def _cancel_after_events( self, widget: tk.Widget, cancelled: set[str] | None = None ) -> None: - """Wrapper for :func:`cancel_after_events` for backward compatibility.""" + """Wrapper for :func:`cancel_after_events`. + + Cancels Tk ``after`` callbacks tied to *widget* to avoid orphaned Tcl + commands such as ``*_animate``. + """ cancel_after_events(widget, cancelled) @@ -1354,6 +1368,12 @@ def _raise_widgets( def _detach_tab(self, tab_id: str, x: int, y: int) -> None: + """Detach *tab_id* into a new floating window at *(x, y)*. + + Tk ``after`` callbacks tied to the original tab are cancelled before it + is forgotten to prevent orphaned Tcl commands such as ``*_animate``. + """ + from .detached_window import DetachedWindow self.update_idletasks()