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
9 changes: 9 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
-->

## 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
Expand Down
8 changes: 6 additions & 2 deletions gui/utils/closable_notebook.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# GNU disclaimer
# Author: Miguel Marina <karel.capek.robotics@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
#
Expand Down Expand Up @@ -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."""
Expand Down
74 changes: 74 additions & 0 deletions tests/detachment/window/test_detached_window_moved_widget.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# GNU disclaimer
# Author: Miguel Marina <karel.capek.robotics@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
#
Expand All @@ -24,6 +25,7 @@

import pytest

from gui.utils.closable_notebook import ClosableNotebook
from gui.utils.detached_window import DetachedWindow


Expand Down Expand Up @@ -81,3 +83,75 @@ def test_selector_event_triggers_switch(self) -> None:
diagram.toolbox_selector.event_generate("<<ComboboxSelected>>")
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("<<HostWindowResized>>", 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()