diff --git a/mytk/app.py b/mytk/app.py index 4e5ce79..994ef15 100644 --- a/mytk/app.py +++ b/mytk/app.py @@ -112,6 +112,26 @@ def widget(self): """ return self.root + def widget_hierarchy(self, widget=None): + """Return the tkinter widget tree as a nested dict, using only + tkinter's built-in winfo_* introspection methods. + + Args: + widget: The tkinter widget to root the tree at. Defaults to the + application's root window. + """ + if widget is None: + widget = self.root + + def node(w): + return { + "class": w.winfo_class(), # e.g. "TButton", "TLabel", "Frame" + "name": w.winfo_name(), # last path component + "path": str(w), # full Tk path, e.g. ".!frame.!button2" + "children": [node(c) for c in w.winfo_children()], + } + return node(widget) + @property def root(self): """Returns the root window widget. diff --git a/mytk/base.py b/mytk/base.py index 94e0d02..b88c81c 100644 --- a/mytk/base.py +++ b/mytk/base.py @@ -79,6 +79,11 @@ def _bind_destroy_cancel(self): """No-op fallback. Overridden by EventCapable when present in the MRO.""" pass + @staticmethod + def view_for(widget): + """Return the mytk View that owns this tkinter widget, or None.""" + return getattr(widget, "mytk_view", None) + """ Placing widgets in other widgets """ @@ -152,6 +157,8 @@ def grid_into( widget = parent.widget self._bind_destroy_cancel() + if self.widget is not None: + self.widget.mytk_view = self self._apply_background_color() column = 0 @@ -253,6 +260,8 @@ def pack_into(self, parent, **kwargs): self.create_widget(master=parent.widget) self.parent = parent self._bind_destroy_cancel() + if self.widget is not None: + self.widget.mytk_view = self self._apply_background_color() if self.widget is not None: @@ -271,6 +280,8 @@ def place_into(self, parent, x, y, width, height): self.create_widget(master=parent.widget) self.parent = parent self._bind_destroy_cancel() + if self.widget is not None: + self.widget.mytk_view = self self._apply_background_color() if self.widget is not None: diff --git a/mytk/tests/testWidgetHierarchy.py b/mytk/tests/testWidgetHierarchy.py new file mode 100644 index 0000000..bc1f0f1 --- /dev/null +++ b/mytk/tests/testWidgetHierarchy.py @@ -0,0 +1,70 @@ +import unittest + +import envtest + +from mytk import * + + +class TestWidget(Base): + def create_widget(self, master): + self.parent = master + self.widget = ttk.Frame(master, width=100, height=100) + + +class TestViewFor(envtest.MyTkTestCase): + def test_view_for_returns_owning_view(self): + widget = TestWidget() + widget.grid_into(self.app.window, row=1, column=0) + self.assertIs(Base.view_for(widget.widget), widget) + + def test_view_for_after_pack(self): + # The app window already manages slaves with grid, so pack into a + # fresh empty container to avoid a geometry-manager conflict. + container = View(width=100, height=100) + container.grid_into(self.app.window, row=1, column=0) + widget = TestWidget() + widget.pack_into(container) + self.assertIs(Base.view_for(widget.widget), widget) + + def test_view_for_after_place(self): + widget = TestWidget() + widget.place_into(self.app.window, x=0, y=0, width=50, height=50) + self.assertIs(Base.view_for(widget.widget), widget) + + def test_view_for_unknown_widget_returns_none(self): + bare = ttk.Frame(self.app.window.widget) + self.assertIsNone(Base.view_for(bare)) + + +class TestWidgetHierarchy(envtest.MyTkTestCase): + def test_hierarchy_defaults_to_root(self): + tree = self.app.widget_hierarchy() + self.assertEqual(tree["path"], ".") + self.assertIn("class", tree) + self.assertIn("name", tree) + self.assertIsInstance(tree["children"], list) + + def test_hierarchy_includes_added_widget(self): + widget = TestWidget() + widget.grid_into(self.app.window, row=1, column=0) + + tree = self.app.widget_hierarchy() + + def paths(node): + yield node["path"] + for child in node["children"]: + yield from paths(child) + + self.assertIn(str(widget.widget), list(paths(tree))) + + def test_hierarchy_custom_root(self): + widget = TestWidget() + widget.grid_into(self.app.window, row=1, column=0) + + tree = self.app.widget_hierarchy(widget.widget) + self.assertEqual(tree["path"], str(widget.widget)) + self.assertEqual(tree["class"], widget.widget.winfo_class()) + + +if __name__ == "__main__": + unittest.main()