diff --git a/HISTORY.md b/HISTORY.md index 43d24069..626e1fba 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -18,6 +18,15 @@ # along with this program. If not, see . --> +## 0.2.283 - 2025-09-19 + +- Ensure detached notebook tabs register with the floating window helper so + resize tracking and lifecycle hooks initialise together, preserving toolbox + activation after detachment. +- Extend the detachment window regression suite with a resize scenario that + confirms floating windows propagate geometry updates to their hosted content + independently of the originating notebook. + ## 0.2.282 - 2025-09-18 - Harden ``cancel_after_events`` so it cancels pending Tk ``after`` jobs before diff --git a/gui/utils/closable_notebook.py b/gui/utils/closable_notebook.py index cbfa4de8..7d666fa4 100644 --- a/gui/utils/closable_notebook.py +++ b/gui/utils/closable_notebook.py @@ -1,3 +1,4 @@ +# GNU disclaimer # Author: Miguel Marina # SPDX-License-Identifier: GPL-3.0-or-later # @@ -941,8 +942,11 @@ def _on_destroy(_e, w=dw.win) -> None: except tk.TclError: logger.exception("Failed to detach tab %s", tab_id) return - dw._ensure_toolbox(child) - dw._activate_hooks(child) + try: + text = dw.nb.tab(child, "text") + except Exception: + text = "" + dw.add_moved_widget(child, text) def rewrite_option_references(self, mapping: dict[tk.Widget, tk.Widget]) -> None: """Rewrite widget configuration options to point at cloned widgets.""" diff --git a/tests/detachment/window/test_detached_window_moved_widget.py b/tests/detachment/window/test_detached_window_moved_widget.py index de07b85c..c3107747 100644 --- a/tests/detachment/window/test_detached_window_moved_widget.py +++ b/tests/detachment/window/test_detached_window_moved_widget.py @@ -1,3 +1,4 @@ +# GNU disclaimer # Author: Miguel Marina # SPDX-License-Identifier: GPL-3.0-or-later # @@ -24,6 +25,7 @@ import pytest +from gui.utils.closable_notebook import ClosableNotebook from gui.utils.detached_window import DetachedWindow @@ -81,3 +83,75 @@ def test_selector_event_triggers_switch(self) -> None: diagram.toolbox_selector.event_generate("<>") assert diagram.log.count("switch") == count + 1 root.destroy() + + def test_detached_content_tracks_floating_window_resize( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + try: + root = tk.Tk() + except tk.TclError: + pytest.skip("Tk not available") + + nb = ClosableNotebook(root) + nb.pack(expand=True, fill="both") + + class TrackingDetachedWindow(DetachedWindow): + created: list["TrackingDetachedWindow"] = [] + + def __init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003 - signature from parent + super().__init__(*args, **kwargs) + TrackingDetachedWindow.created.append(self) + + monkeypatch.setattr( + "gui.utils.closable_notebook.DetachedWindow", TrackingDetachedWindow + ) + TrackingDetachedWindow.created.clear() + + class LoggedFrame(tk.Frame): + def __init__(self, master: tk.Misc) -> None: + super().__init__(master) + self.events: list[str] = [] + self.bind("<>", self._on_host_resize, add="+") + + def _on_host_resize(self, event: tk.Event) -> None: # pragma: no cover - Tk callback + data = getattr(event, "data", "") + if not data: + width = getattr(event, "width", 0) + height = getattr(event, "height", 0) + data = f"{width}x{height}" + self.events.append(str(data)) + + frame = LoggedFrame(nb) + nb.add(frame, text="Detached") + root.update_idletasks() + + tabs = nb.tabs() + assert tabs + nb._detach_tab(tabs[0], 120, 160) + + assert TrackingDetachedWindow.created + detached = TrackingDetachedWindow.created[-1] + win = detached.win + win.update_idletasks() + + frame.events.clear() + win.geometry("420x315+40+40") + win.update() + win.update_idletasks() + + assert frame.events, "Detached widget did not receive resize notification" + size_tokens = frame.events[-1].split("x") + assert len(size_tokens) == 2 + width, height = map(int, size_tokens) + assert width == 420 + assert height == 315 + assert frame.winfo_width() == 420 + + nb.configure(width=200, height=180) + root.update_idletasks() + assert frame.events[-1] == "420x315" + assert frame.winfo_width() == 420 + + if detached.win.winfo_exists(): + detached.win.destroy() + root.destroy()