diff --git a/HISTORY.md b/HISTORY.md
index b9bc30d7..9e382a74 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -18,6 +18,11 @@
# along with this program. If not, see .
-->
+## 0.2.252 - 2025-09-04
+
+- Rebuild detached tabs by cloning widgets instead of reparenting to avoid
+ cross-toplevel ``TclError``
+
## 0.2.251 - 2025-09-04
- Reparent detached tabs across toplevel windows using native OS APIs so
diff --git a/gui/utils/widget_transfer_manager.py b/gui/utils/widget_transfer_manager.py
index 590cc7c9..36d93576 100644
--- a/gui/utils/widget_transfer_manager.py
+++ b/gui/utils/widget_transfer_manager.py
@@ -16,7 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-"""Utilities for moving widgets between notebooks without cloning."""
+"""Utilities for moving widgets between notebooks by cloning."""
from __future__ import annotations
@@ -24,9 +24,9 @@
import typing as t
try: # pragma: no cover - support direct module execution
- from .tk_utils import cancel_after_events, reparent_widget
+ from .tk_utils import cancel_after_events
except Exception: # pragma: no cover - legacy path
- from tk_utils import cancel_after_events, reparent_widget
+ from tk_utils import cancel_after_events
class WidgetTransferManager:
@@ -58,19 +58,78 @@ def detach_tab(
orig = source.nametowidget(tab_id)
text = source.tab(tab_id, "text")
cancel_after_events(orig)
+ clone = self._clone_widget(orig, target)
source.forget(orig)
try:
- reparent_widget(orig, target)
- target.add(orig, text=text)
- target.select(orig)
- except tk.TclError as exc:
- # Roll back to the source notebook if re-parenting fails
+ orig.destroy()
+ except Exception:
+ pass
+ target.add(clone, text=text)
+ target.select(clone)
+ return clone
+
+ # ------------------------------------------------------------------
+ # Cloning helpers
+ # ------------------------------------------------------------------
+
+ def _clone_widget(self, widget: tk.Widget, parent: tk.Widget) -> tk.Widget:
+ """Recursively clone *widget* under *parent*."""
+
+ cls: t.Type[tk.Widget] = widget.__class__
+ clone = cls(parent)
+
+ # Copy configuration options
+ try:
+ config = widget.configure()
+ except tk.TclError:
+ config = {}
+ for opt, opts in config.items():
+ if isinstance(opts, (tuple, list)) and len(opts) >= 2:
+ value = opts[-1]
+ try:
+ clone.configure({opt: value})
+ except tk.TclError:
+ pass
+
+ # Copy event bindings
+ try:
+ sequences = widget.tk.call("bind", widget._w).split()
+ except Exception:
+ sequences = []
+ for seq in sequences:
try:
- target.forget(orig)
- except tk.TclError:
- pass
- source.add(orig, text=text)
- source.select(orig)
- raise exc
-
- return orig
+ cmd = widget.bind(seq)
+ if cmd:
+ clone.bind(seq, cmd)
+ except Exception:
+ continue
+
+ # Recreate children
+ for child in widget.winfo_children():
+ child_clone = self._clone_widget(child, clone)
+ self._apply_layout(child, child_clone)
+
+ return clone
+
+ def _apply_layout(self, orig: tk.Widget, clone: tk.Widget) -> None:
+ """Apply geometry management of *orig* to *clone*."""
+
+ manager = orig.winfo_manager()
+ try:
+ if manager == "pack":
+ info = orig.pack_info()
+ for key in ("in", "in_"):
+ info.pop(key, None)
+ clone.pack(**info)
+ elif manager == "grid":
+ info = orig.grid_info()
+ for key in ("in", "in_"):
+ info.pop(key, None)
+ clone.grid(**info)
+ elif manager == "place":
+ info = orig.place_info()
+ for key in ("in", "in_"):
+ info.pop(key, None)
+ clone.place(**info)
+ except Exception:
+ pass
diff --git a/tests/detachment/window/test_reparent_across_toplevel.py b/tests/detachment/window/test_reparent_across_toplevel.py
index af8a3c2a..8b6c6129 100644
--- a/tests/detachment/window/test_reparent_across_toplevel.py
+++ b/tests/detachment/window/test_reparent_across_toplevel.py
@@ -16,7 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-"""Regression test for reparenting tabs across toplevel windows."""
+"""Regression test for cloning tabs across toplevel windows."""
from __future__ import annotations
@@ -25,7 +25,6 @@
import pytest
-import os
from gui.utils.closable_notebook import ClosableNotebook
from gui.utils.widget_transfer_manager import WidgetTransferManager
@@ -33,9 +32,7 @@
@pytest.mark.detachment
@pytest.mark.reparenting
class TestReparentAcrossToplevel:
- def test_widget_reparented_between_toplevels(self) -> None:
- if os.name != "nt":
- pytest.skip("OS-level reparenting implemented only on Windows")
+ def test_widget_cloned_between_toplevels(self) -> None:
try:
root = tk.Tk()
except tk.TclError:
@@ -49,8 +46,9 @@ def test_widget_reparented_between_toplevels(self) -> None:
nb2.pack()
tab_id = nb1.tabs()[0]
manager = WidgetTransferManager()
- moved = manager.detach_tab(nb1, tab_id, nb2)
- assert moved is frame
- assert nb2.nametowidget(nb2.tabs()[0]) is frame
- assert frame.master is nb2
+ clone = manager.detach_tab(nb1, tab_id, nb2)
+ assert clone is not frame
+ assert isinstance(clone, ttk.Frame)
+ assert not frame.winfo_exists()
+ assert nb2.nametowidget(nb2.tabs()[0]) is clone
root.destroy()