We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Modifying Tree Widget nodes before the Tree is Expanded/Drawn in macOS
### Describe the bug

If you attempt to append, insert, remove, or modify a child node used as a data source for the Tree widget, before it has expanded and displayed those nodes, an **AttributeError: 'Node' object has no attribute '_impl'** will be generated. It appears that the child nodes are being generated on demand and don't exist until they have been displayed. A work around is to manually expand the Tree before doing any modifications.

### Steps to reproduce

A minimum script to reproduce the error is shown below. If you comment out `tree.expand()` the error will be thrown.

```
import toga

def build(app):
    tree = toga.Tree(headings=["Type", "Text", "Options", "ID"])
    root_node = tree.data.append(dict(type="Root Node", text="", options="", ID="0"))
    leaf_node = dict(type="Spelling", text="word", options="opt", id="0", root=False)
    tree.expand()
    #root_node.append(leaf_node)
    root_node.insert(0, leaf_node)
    return tree

def main():
    return toga.App("Node Test App", "au.com.reefwing.node_test", startup=build)

if __name__ == "__main__":
    main().main_loop()
```

### Expected behavior

It should be possible to modify the underlying Tree data model regardless of the state of the UI widget.

### Screenshots

_No response_

### Environment

- Operating System: macOS
- Python version: 3.11
- Software versions:
  - Briefcase:
  - Toga: 0.4.2


### Logs

```
File "/Users/dsuch/Documents/BeeWare/tests/node_test.py", line 14, in build
    root_node.insert(0, leaf_node)
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/toga/sources/tree_source.py", line 122, in insert
    self._source.notify("insert", parent=self, index=index, item=node)
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/toga/sources/base.py", line 84, in notify
    method(**kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/toga_cocoa/widgets/tree.py", line 223, in insert
    inParent=parent._impl if parent else None,
             ^^^^^^^^^^^^
AttributeError: 'Node' object has no attribute '_impl'
```


### Additional context

_No response_

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `cocoa/src/toga_cocoa/widgets/tree.py`
Content:
```
1 from rubicon.objc import SEL, at, objc_method, objc_property
2 from travertino.size import at_least
3 
4 import toga
5 from toga_cocoa.libs import (
6     NSBezelBorder,
7     NSIndexSet,
8     NSOutlineView,
9     NSScrollView,
10     NSTableColumn,
11     NSTableViewAnimation,
12     NSTableViewColumnAutoresizingStyle,
13 )
14 from toga_cocoa.widgets.base import Widget
15 from toga_cocoa.widgets.internal.cells import TogaIconView
16 from toga_cocoa.widgets.internal.data import TogaData
17 
18 
19 class TogaTree(NSOutlineView):
20     interface = objc_property(object, weak=True)
21     impl = objc_property(object, weak=True)
22 
23     # OutlineViewDataSource methods
24     @objc_method
25     def outlineView_child_ofItem_(self, tree, child: int, item):
26         # Get the object representing the row
27         if item is None:
28             node = self.interface.data[child]
29         else:
30             node = item.attrs["node"][child]
31 
32         # Get the Cocoa implementation for the row. If an _impl
33         # doesn't exist, create a data object for it, and
34         # populate it with initial values for each column.
35         try:
36             node_impl = node._impl
37         except AttributeError:
38             node_impl = TogaData.alloc().init()
39             node_impl.attrs = {"node": node}
40             node._impl = node_impl
41 
42         return node_impl
43 
44     @objc_method
45     def outlineView_isItemExpandable_(self, tree, item) -> bool:
46         return item.attrs["node"].can_have_children()
47 
48     @objc_method
49     def outlineView_numberOfChildrenOfItem_(self, tree, item) -> int:
50         if item is None:
51             # How many root elements are there?
52             # If we're starting up, the source may not exist yet.
53             if self.interface.data is not None:
54                 return len(self.interface.data)
55             else:
56                 return 0
57         else:
58             # How many children does this node have?
59             return len(item.attrs["node"])
60 
61     @objc_method
62     def outlineView_viewForTableColumn_item_(self, tree, column, item):
63         col_identifier = str(column.identifier)
64 
65         try:
66             value = getattr(item.attrs["node"], col_identifier)
67 
68             # if the value is a widget itself, just draw the widget!
69             if isinstance(value, toga.Widget):
70                 return value._impl.native
71 
72             # Allow for an (icon, value) tuple as the simple case
73             # for encoding an icon in a table cell. Otherwise, look
74             # for an icon attribute.
75             elif isinstance(value, tuple):
76                 icon, value = value
77             else:
78                 try:
79                     icon = value.icon
80                 except AttributeError:
81                     icon = None
82         except AttributeError:
83             # If the node doesn't have a property with the
84             # accessor name, assume an empty string value.
85             value = self.interface.missing_value
86             icon = None
87 
88         # creates a NSTableCellView from interface-builder template (does not exist)
89         # or reuses an existing view which is currently not needed for painting
90         # returns None (nil) if both fails
91         identifier = at(f"CellView_{self.interface.id}")
92         tcv = self.makeViewWithIdentifier(identifier, owner=self)
93 
94         if not tcv:  # there is no existing view to reuse so create a new one
95             # tcv = TogaIconView.alloc().initWithFrame(CGRectMake(0, 0, column.width, 16))
96             tcv = TogaIconView.alloc().init()
97             tcv.identifier = identifier
98 
99             # Prevent tcv from being deallocated prematurely when no Python references
100             # are left
101             tcv.retain()
102             tcv.autorelease()
103 
104         tcv.setText(str(value))
105         if icon:
106             tcv.setImage(icon._impl.native)
107         else:
108             tcv.setImage(None)
109 
110         return tcv
111 
112     # 2023-06-29: Commented out this method because it appears to be a
113     # source of significant slowdown when the table has a lot of data
114     # (10k rows). AFAICT, it's only needed if we want custom row heights
115     # for each row. Since we don't currently support custom row heights,
116     # we're paying the cost for no benefit.
117     # @objc_method
118     # def outlineView_heightOfRowByItem_(self, tree, item) -> float:
119     #     default_row_height = self.rowHeight
120 
121     #     if item is self:
122     #         return default_row_height
123 
124     #     heights = [default_row_height]
125 
126     #     for column in self.tableColumns:
127     #         value = getattr(
128     #             item.attrs["node"], str(column.identifier), self.interface.missing_value
129     #         )
130 
131     #         if isinstance(value, toga.Widget):
132     #             # if the cell value is a widget, use its height
133     #             heights.append(value._impl.native.intrinsicContentSize().height)
134 
135     #     return max(heights)
136 
137     @objc_method
138     def outlineView_pasteboardWriterForItem_(
139         self, tree, item
140     ) -> None:  # pragma: no cover
141         # this seems to be required to prevent issue 21562075 in AppKit
142         return None
143 
144     # @objc_method
145     # def outlineView_sortDescriptorsDidChange_(self, tableView, oldDescriptors) -> None:
146     #
147     #     for descriptor in self.sortDescriptors[::-1]:
148     #         accessor = descriptor.key
149     #         reverse = descriptor.ascending == 1
150     #         key = self.interface._sort_keys[str(accessor)]
151     #         try:
152     #             self.interface.data.sort(str(accessor), reverse=reverse, key=key)
153     #         except AttributeError:
154     #             pass
155     #         else:
156     #             self.reloadData()
157 
158     # OutlineViewDelegate methods
159     @objc_method
160     def outlineViewSelectionDidChange_(self, notification) -> None:
161         self.interface.on_select()
162 
163     # target methods
164     @objc_method
165     def onDoubleClick_(self, sender) -> None:
166         node = self.itemAtRow(self.clickedRow).attrs["node"]
167         self.interface.on_activate(node=node)
168 
169 
170 class Tree(Widget):
171     def create(self):
172         # Create a tree view, and put it in a scroll view.
173         # The scroll view is the _impl, because it's the outer container.
174         self.native = NSScrollView.alloc().init()
175         self.native.hasVerticalScroller = True
176         self.native.hasHorizontalScroller = False
177         self.native.autohidesScrollers = False
178         self.native.borderType = NSBezelBorder
179 
180         # Create the Tree widget
181         self.native_tree = TogaTree.alloc().init()
182         self.native_tree.interface = self.interface
183         self.native_tree.impl = self
184         self.native_tree.columnAutoresizingStyle = (
185             NSTableViewColumnAutoresizingStyle.Uniform
186         )
187         self.native_tree.usesAlternatingRowBackgroundColors = True
188         self.native_tree.allowsMultipleSelection = self.interface.multiple_select
189 
190         # Create columns for the table
191         self.columns = []
192         if self.interface.headings:
193             for index, (heading, accessor) in enumerate(
194                 zip(self.interface.headings, self.interface.accessors)
195             ):
196                 self._insert_column(index, heading, accessor)
197         else:
198             self.native_tree.setHeaderView(None)
199             for index, accessor in enumerate(self.interface.accessors):
200                 self._insert_column(index, None, accessor)
201 
202         # Put the tree arrows in the first column.
203         self.native_tree.outlineTableColumn = self.columns[0]
204 
205         self.native_tree.delegate = self.native_tree
206         self.native_tree.dataSource = self.native_tree
207         self.native_tree.target = self.native_tree
208         self.native_tree.doubleAction = SEL("onDoubleClick:")
209 
210         # Embed the tree view in the scroll view
211         self.native.documentView = self.native_tree
212 
213         # Add the layout constraints
214         self.add_constraints()
215 
216     def change_source(self, source):
217         self.native_tree.reloadData()
218 
219     def insert(self, parent, index, item):
220         index_set = NSIndexSet.indexSetWithIndex(index)
221         self.native_tree.insertItemsAtIndexes(
222             index_set,
223             inParent=parent._impl if parent else None,
224             withAnimation=NSTableViewAnimation.SlideDown.value,
225         )
226 
227     def change(self, item):
228         self.native_tree.reloadItem(item._impl)
229 
230     def remove(self, parent, index, item):
231         index = self.native_tree.childIndexForItem(item._impl)
232         index_set = NSIndexSet.indexSetWithIndex(index)
233         parent = self.native_tree.parentForItem(item._impl)
234         self.native_tree.removeItemsAtIndexes(
235             index_set,
236             inParent=parent,
237             withAnimation=NSTableViewAnimation.SlideUp.value,
238         )
239 
240     def clear(self):
241         self.native_tree.reloadData()
242 
243     def get_selection(self):
244         if self.interface.multiple_select:
245             selection = []
246 
247             current_index = self.native_tree.selectedRowIndexes.firstIndex
248             for i in range(self.native_tree.selectedRowIndexes.count):
249                 selection.append(
250                     self.native_tree.itemAtRow(current_index).attrs["node"]
251                 )
252                 current_index = (
253                     self.native_tree.selectedRowIndexes.indexGreaterThanIndex(
254                         current_index
255                     )
256                 )
257 
258             return selection
259         else:
260             index = self.native_tree.selectedRow
261             if index != -1:
262                 return self.native_tree.itemAtRow(index).attrs["node"]
263             else:
264                 return None
265 
266     def expand_node(self, node):
267         self.native_tree.expandItem(node._impl, expandChildren=True)
268 
269     def expand_all(self):
270         self.native_tree.expandItem(None, expandChildren=True)
271 
272     def collapse_node(self, node):
273         self.native_tree.collapseItem(node._impl, collapseChildren=True)
274 
275     def collapse_all(self):
276         self.native_tree.collapseItem(None, collapseChildren=True)
277 
278     def _insert_column(self, index, heading, accessor):
279         column = NSTableColumn.alloc().initWithIdentifier(accessor)
280         column.minWidth = 16
281 
282         self.columns.insert(index, column)
283         self.native_tree.addTableColumn(column)
284         if index != len(self.columns) - 1:
285             self.native_tree.moveColumn(len(self.columns) - 1, toColumn=index)
286 
287         if heading is not None:
288             column.headerCell.stringValue = heading
289 
290     def insert_column(self, index, heading, accessor):
291         self._insert_column(index, heading, accessor)
292         self.native_tree.sizeToFit()
293 
294     def remove_column(self, index):
295         column = self.columns[index]
296         self.native_tree.removeTableColumn(column)
297 
298         # delete column and identifier
299         self.columns.remove(column)
300         self.native_tree.sizeToFit()
301 
302     def rehint(self):
303         self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH)
304         self.interface.intrinsic.height = at_least(self.interface._MIN_HEIGHT)
305
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/cocoa/src/toga_cocoa/widgets/tree.py b/cocoa/src/toga_cocoa/widgets/tree.py
--- a/cocoa/src/toga_cocoa/widgets/tree.py
+++ b/cocoa/src/toga_cocoa/widgets/tree.py
@@ -16,6 +16,15 @@
 from toga_cocoa.widgets.internal.data import TogaData
 
 
+def node_impl(node):
+    try:
+        return node._impl
+    except AttributeError:
+        node._impl = TogaData.alloc().init()
+        node._impl.attrs = {"node": node}
+        return node._impl
+
+
 class TogaTree(NSOutlineView):
     interface = objc_property(object, weak=True)
     impl = objc_property(object, weak=True)
@@ -32,14 +41,8 @@
         # Get the Cocoa implementation for the row. If an _impl
         # doesn't exist, create a data object for it, and
         # populate it with initial values for each column.
-        try:
-            node_impl = node._impl
-        except AttributeError:
-            node_impl = TogaData.alloc().init()
-            node_impl.attrs = {"node": node}
-            node._impl = node_impl
 
-        return node_impl
+        return node_impl(node)
 
     @objc_method
     def outlineView_isItemExpandable_(self, tree, item) -> bool:
@@ -220,22 +223,25 @@
         index_set = NSIndexSet.indexSetWithIndex(index)
         self.native_tree.insertItemsAtIndexes(
             index_set,
-            inParent=parent._impl if parent else None,
+            inParent=node_impl(parent) if parent else None,
             withAnimation=NSTableViewAnimation.SlideDown.value,
         )
 
     def change(self, item):
-        self.native_tree.reloadItem(item._impl)
+        self.native_tree.reloadItem(node_impl(item))
 
     def remove(self, parent, index, item):
-        index = self.native_tree.childIndexForItem(item._impl)
-        index_set = NSIndexSet.indexSetWithIndex(index)
-        parent = self.native_tree.parentForItem(item._impl)
-        self.native_tree.removeItemsAtIndexes(
-            index_set,
-            inParent=parent,
-            withAnimation=NSTableViewAnimation.SlideUp.value,
-        )
+        try:
+            index = self.native_tree.childIndexForItem(item._impl)
+            index_set = NSIndexSet.indexSetWithIndex(index)
+            parent = self.native_tree.parentForItem(item._impl)
+            self.native_tree.removeItemsAtIndexes(
+                index_set,
+                inParent=parent,
+                withAnimation=NSTableViewAnimation.SlideUp.value,
+            )
+        except AttributeError:
+            pass
 
     def clear(self):
         self.native_tree.reloadData()
@@ -264,13 +270,13 @@
                 return None
 
     def expand_node(self, node):
-        self.native_tree.expandItem(node._impl, expandChildren=True)
+        self.native_tree.expandItem(node_impl(node), expandChildren=True)
 
     def expand_all(self):
         self.native_tree.expandItem(None, expandChildren=True)
 
     def collapse_node(self, node):
-        self.native_tree.collapseItem(node._impl, collapseChildren=True)
+        self.native_tree.collapseItem(node_impl(node), collapseChildren=True)
 
     def collapse_all(self):
         self.native_tree.collapseItem(None, collapseChildren=True)

{"golden_diff": "diff --git a/cocoa/src/toga_cocoa/widgets/tree.py b/cocoa/src/toga_cocoa/widgets/tree.py\n--- a/cocoa/src/toga_cocoa/widgets/tree.py\n+++ b/cocoa/src/toga_cocoa/widgets/tree.py\n@@ -16,6 +16,15 @@\n from toga_cocoa.widgets.internal.data import TogaData\n \n \n+def node_impl(node):\n+    try:\n+        return node._impl\n+    except AttributeError:\n+        node._impl = TogaData.alloc().init()\n+        node._impl.attrs = {\"node\": node}\n+        return node._impl\n+\n+\n class TogaTree(NSOutlineView):\n     interface = objc_property(object, weak=True)\n     impl = objc_property(object, weak=True)\n@@ -32,14 +41,8 @@\n         # Get the Cocoa implementation for the row. If an _impl\n         # doesn't exist, create a data object for it, and\n         # populate it with initial values for each column.\n-        try:\n-            node_impl = node._impl\n-        except AttributeError:\n-            node_impl = TogaData.alloc().init()\n-            node_impl.attrs = {\"node\": node}\n-            node._impl = node_impl\n \n-        return node_impl\n+        return node_impl(node)\n \n     @objc_method\n     def outlineView_isItemExpandable_(self, tree, item) -> bool:\n@@ -220,22 +223,25 @@\n         index_set = NSIndexSet.indexSetWithIndex(index)\n         self.native_tree.insertItemsAtIndexes(\n             index_set,\n-            inParent=parent._impl if parent else None,\n+            inParent=node_impl(parent) if parent else None,\n             withAnimation=NSTableViewAnimation.SlideDown.value,\n         )\n \n     def change(self, item):\n-        self.native_tree.reloadItem(item._impl)\n+        self.native_tree.reloadItem(node_impl(item))\n \n     def remove(self, parent, index, item):\n-        index = self.native_tree.childIndexForItem(item._impl)\n-        index_set = NSIndexSet.indexSetWithIndex(index)\n-        parent = self.native_tree.parentForItem(item._impl)\n-        self.native_tree.removeItemsAtIndexes(\n-            index_set,\n-            inParent=parent,\n-            withAnimation=NSTableViewAnimation.SlideUp.value,\n-        )\n+        try:\n+            index = self.native_tree.childIndexForItem(item._impl)\n+            index_set = NSIndexSet.indexSetWithIndex(index)\n+            parent = self.native_tree.parentForItem(item._impl)\n+            self.native_tree.removeItemsAtIndexes(\n+                index_set,\n+                inParent=parent,\n+                withAnimation=NSTableViewAnimation.SlideUp.value,\n+            )\n+        except AttributeError:\n+            pass\n \n     def clear(self):\n         self.native_tree.reloadData()\n@@ -264,13 +270,13 @@\n                 return None\n \n     def expand_node(self, node):\n-        self.native_tree.expandItem(node._impl, expandChildren=True)\n+        self.native_tree.expandItem(node_impl(node), expandChildren=True)\n \n     def expand_all(self):\n         self.native_tree.expandItem(None, expandChildren=True)\n \n     def collapse_node(self, node):\n-        self.native_tree.collapseItem(node._impl, collapseChildren=True)\n+        self.native_tree.collapseItem(node_impl(node), collapseChildren=True)\n \n     def collapse_all(self):\n         self.native_tree.collapseItem(None, collapseChildren=True)\n", "issue": "Modifying Tree Widget nodes before the Tree is Expanded/Drawn in macOS\n### Describe the bug\n\nIf you attempt to append, insert, remove, or modify a child node used as a data source for the Tree widget, before it has expanded and displayed those nodes, an **AttributeError: 'Node' object has no attribute '_impl'** will be generated. It appears that the child nodes are being generated on demand and don't exist until they have been displayed. A work around is to manually expand the Tree before doing any modifications.\n\n### Steps to reproduce\n\nA minimum script to reproduce the error is shown below. If you comment out `tree.expand()` the error will be thrown.\r\n\r\n```\r\nimport toga\r\n\r\ndef build(app):\r\n    tree = toga.Tree(headings=[\"Type\", \"Text\", \"Options\", \"ID\"])\r\n    root_node = tree.data.append(dict(type=\"Root Node\", text=\"\", options=\"\", ID=\"0\"))\r\n    leaf_node = dict(type=\"Spelling\", text=\"word\", options=\"opt\", id=\"0\", root=False)\r\n    tree.expand()\r\n    #root_node.append(leaf_node)\r\n    root_node.insert(0, leaf_node)\r\n    return tree\r\n\r\ndef main():\r\n    return toga.App(\"Node Test App\", \"au.com.reefwing.node_test\", startup=build)\r\n\r\nif __name__ == \"__main__\":\r\n    main().main_loop()\r\n```\n\n### Expected behavior\n\nIt should be possible to modify the underlying Tree data model regardless of the state of the UI widget.\n\n### Screenshots\n\n_No response_\n\n### Environment\n\n- Operating System: macOS\r\n- Python version: 3.11\r\n- Software versions:\r\n  - Briefcase:\r\n  - Toga: 0.4.2\r\n\n\n### Logs\n\n```\r\nFile \"/Users/dsuch/Documents/BeeWare/tests/node_test.py\", line 14, in build\r\n    root_node.insert(0, leaf_node)\r\n  File \"/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/toga/sources/tree_source.py\", line 122, in insert\r\n    self._source.notify(\"insert\", parent=self, index=index, item=node)\r\n  File \"/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/toga/sources/base.py\", line 84, in notify\r\n    method(**kwargs)\r\n  File \"/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/toga_cocoa/widgets/tree.py\", line 223, in insert\r\n    inParent=parent._impl if parent else None,\r\n             ^^^^^^^^^^^^\r\nAttributeError: 'Node' object has no attribute '_impl'\r\n```\r\n\n\n### Additional context\n\n_No response_\n", "before_files": [{"content": "from rubicon.objc import SEL, at, objc_method, objc_property\nfrom travertino.size import at_least\n\nimport toga\nfrom toga_cocoa.libs import (\n    NSBezelBorder,\n    NSIndexSet,\n    NSOutlineView,\n    NSScrollView,\n    NSTableColumn,\n    NSTableViewAnimation,\n    NSTableViewColumnAutoresizingStyle,\n)\nfrom toga_cocoa.widgets.base import Widget\nfrom toga_cocoa.widgets.internal.cells import TogaIconView\nfrom toga_cocoa.widgets.internal.data import TogaData\n\n\nclass TogaTree(NSOutlineView):\n    interface = objc_property(object, weak=True)\n    impl = objc_property(object, weak=True)\n\n    # OutlineViewDataSource methods\n    @objc_method\n    def outlineView_child_ofItem_(self, tree, child: int, item):\n        # Get the object representing the row\n        if item is None:\n            node = self.interface.data[child]\n        else:\n            node = item.attrs[\"node\"][child]\n\n        # Get the Cocoa implementation for the row. If an _impl\n        # doesn't exist, create a data object for it, and\n        # populate it with initial values for each column.\n        try:\n            node_impl = node._impl\n        except AttributeError:\n            node_impl = TogaData.alloc().init()\n            node_impl.attrs = {\"node\": node}\n            node._impl = node_impl\n\n        return node_impl\n\n    @objc_method\n    def outlineView_isItemExpandable_(self, tree, item) -> bool:\n        return item.attrs[\"node\"].can_have_children()\n\n    @objc_method\n    def outlineView_numberOfChildrenOfItem_(self, tree, item) -> int:\n        if item is None:\n            # How many root elements are there?\n            # If we're starting up, the source may not exist yet.\n            if self.interface.data is not None:\n                return len(self.interface.data)\n            else:\n                return 0\n        else:\n            # How many children does this node have?\n            return len(item.attrs[\"node\"])\n\n    @objc_method\n    def outlineView_viewForTableColumn_item_(self, tree, column, item):\n        col_identifier = str(column.identifier)\n\n        try:\n            value = getattr(item.attrs[\"node\"], col_identifier)\n\n            # if the value is a widget itself, just draw the widget!\n            if isinstance(value, toga.Widget):\n                return value._impl.native\n\n            # Allow for an (icon, value) tuple as the simple case\n            # for encoding an icon in a table cell. Otherwise, look\n            # for an icon attribute.\n            elif isinstance(value, tuple):\n                icon, value = value\n            else:\n                try:\n                    icon = value.icon\n                except AttributeError:\n                    icon = None\n        except AttributeError:\n            # If the node doesn't have a property with the\n            # accessor name, assume an empty string value.\n            value = self.interface.missing_value\n            icon = None\n\n        # creates a NSTableCellView from interface-builder template (does not exist)\n        # or reuses an existing view which is currently not needed for painting\n        # returns None (nil) if both fails\n        identifier = at(f\"CellView_{self.interface.id}\")\n        tcv = self.makeViewWithIdentifier(identifier, owner=self)\n\n        if not tcv:  # there is no existing view to reuse so create a new one\n            # tcv = TogaIconView.alloc().initWithFrame(CGRectMake(0, 0, column.width, 16))\n            tcv = TogaIconView.alloc().init()\n            tcv.identifier = identifier\n\n            # Prevent tcv from being deallocated prematurely when no Python references\n            # are left\n            tcv.retain()\n            tcv.autorelease()\n\n        tcv.setText(str(value))\n        if icon:\n            tcv.setImage(icon._impl.native)\n        else:\n            tcv.setImage(None)\n\n        return tcv\n\n    # 2023-06-29: Commented out this method because it appears to be a\n    # source of significant slowdown when the table has a lot of data\n    # (10k rows). AFAICT, it's only needed if we want custom row heights\n    # for each row. Since we don't currently support custom row heights,\n    # we're paying the cost for no benefit.\n    # @objc_method\n    # def outlineView_heightOfRowByItem_(self, tree, item) -> float:\n    #     default_row_height = self.rowHeight\n\n    #     if item is self:\n    #         return default_row_height\n\n    #     heights = [default_row_height]\n\n    #     for column in self.tableColumns:\n    #         value = getattr(\n    #             item.attrs[\"node\"], str(column.identifier), self.interface.missing_value\n    #         )\n\n    #         if isinstance(value, toga.Widget):\n    #             # if the cell value is a widget, use its height\n    #             heights.append(value._impl.native.intrinsicContentSize().height)\n\n    #     return max(heights)\n\n    @objc_method\n    def outlineView_pasteboardWriterForItem_(\n        self, tree, item\n    ) -> None:  # pragma: no cover\n        # this seems to be required to prevent issue 21562075 in AppKit\n        return None\n\n    # @objc_method\n    # def outlineView_sortDescriptorsDidChange_(self, tableView, oldDescriptors) -> None:\n    #\n    #     for descriptor in self.sortDescriptors[::-1]:\n    #         accessor = descriptor.key\n    #         reverse = descriptor.ascending == 1\n    #         key = self.interface._sort_keys[str(accessor)]\n    #         try:\n    #             self.interface.data.sort(str(accessor), reverse=reverse, key=key)\n    #         except AttributeError:\n    #             pass\n    #         else:\n    #             self.reloadData()\n\n    # OutlineViewDelegate methods\n    @objc_method\n    def outlineViewSelectionDidChange_(self, notification) -> None:\n        self.interface.on_select()\n\n    # target methods\n    @objc_method\n    def onDoubleClick_(self, sender) -> None:\n        node = self.itemAtRow(self.clickedRow).attrs[\"node\"]\n        self.interface.on_activate(node=node)\n\n\nclass Tree(Widget):\n    def create(self):\n        # Create a tree view, and put it in a scroll view.\n        # The scroll view is the _impl, because it's the outer container.\n        self.native = NSScrollView.alloc().init()\n        self.native.hasVerticalScroller = True\n        self.native.hasHorizontalScroller = False\n        self.native.autohidesScrollers = False\n        self.native.borderType = NSBezelBorder\n\n        # Create the Tree widget\n        self.native_tree = TogaTree.alloc().init()\n        self.native_tree.interface = self.interface\n        self.native_tree.impl = self\n        self.native_tree.columnAutoresizingStyle = (\n            NSTableViewColumnAutoresizingStyle.Uniform\n        )\n        self.native_tree.usesAlternatingRowBackgroundColors = True\n        self.native_tree.allowsMultipleSelection = self.interface.multiple_select\n\n        # Create columns for the table\n        self.columns = []\n        if self.interface.headings:\n            for index, (heading, accessor) in enumerate(\n                zip(self.interface.headings, self.interface.accessors)\n            ):\n                self._insert_column(index, heading, accessor)\n        else:\n            self.native_tree.setHeaderView(None)\n            for index, accessor in enumerate(self.interface.accessors):\n                self._insert_column(index, None, accessor)\n\n        # Put the tree arrows in the first column.\n        self.native_tree.outlineTableColumn = self.columns[0]\n\n        self.native_tree.delegate = self.native_tree\n        self.native_tree.dataSource = self.native_tree\n        self.native_tree.target = self.native_tree\n        self.native_tree.doubleAction = SEL(\"onDoubleClick:\")\n\n        # Embed the tree view in the scroll view\n        self.native.documentView = self.native_tree\n\n        # Add the layout constraints\n        self.add_constraints()\n\n    def change_source(self, source):\n        self.native_tree.reloadData()\n\n    def insert(self, parent, index, item):\n        index_set = NSIndexSet.indexSetWithIndex(index)\n        self.native_tree.insertItemsAtIndexes(\n            index_set,\n            inParent=parent._impl if parent else None,\n            withAnimation=NSTableViewAnimation.SlideDown.value,\n        )\n\n    def change(self, item):\n        self.native_tree.reloadItem(item._impl)\n\n    def remove(self, parent, index, item):\n        index = self.native_tree.childIndexForItem(item._impl)\n        index_set = NSIndexSet.indexSetWithIndex(index)\n        parent = self.native_tree.parentForItem(item._impl)\n        self.native_tree.removeItemsAtIndexes(\n            index_set,\n            inParent=parent,\n            withAnimation=NSTableViewAnimation.SlideUp.value,\n        )\n\n    def clear(self):\n        self.native_tree.reloadData()\n\n    def get_selection(self):\n        if self.interface.multiple_select:\n            selection = []\n\n            current_index = self.native_tree.selectedRowIndexes.firstIndex\n            for i in range(self.native_tree.selectedRowIndexes.count):\n                selection.append(\n                    self.native_tree.itemAtRow(current_index).attrs[\"node\"]\n                )\n                current_index = (\n                    self.native_tree.selectedRowIndexes.indexGreaterThanIndex(\n                        current_index\n                    )\n                )\n\n            return selection\n        else:\n            index = self.native_tree.selectedRow\n            if index != -1:\n                return self.native_tree.itemAtRow(index).attrs[\"node\"]\n            else:\n                return None\n\n    def expand_node(self, node):\n        self.native_tree.expandItem(node._impl, expandChildren=True)\n\n    def expand_all(self):\n        self.native_tree.expandItem(None, expandChildren=True)\n\n    def collapse_node(self, node):\n        self.native_tree.collapseItem(node._impl, collapseChildren=True)\n\n    def collapse_all(self):\n        self.native_tree.collapseItem(None, collapseChildren=True)\n\n    def _insert_column(self, index, heading, accessor):\n        column = NSTableColumn.alloc().initWithIdentifier(accessor)\n        column.minWidth = 16\n\n        self.columns.insert(index, column)\n        self.native_tree.addTableColumn(column)\n        if index != len(self.columns) - 1:\n            self.native_tree.moveColumn(len(self.columns) - 1, toColumn=index)\n\n        if heading is not None:\n            column.headerCell.stringValue = heading\n\n    def insert_column(self, index, heading, accessor):\n        self._insert_column(index, heading, accessor)\n        self.native_tree.sizeToFit()\n\n    def remove_column(self, index):\n        column = self.columns[index]\n        self.native_tree.removeTableColumn(column)\n\n        # delete column and identifier\n        self.columns.remove(column)\n        self.native_tree.sizeToFit()\n\n    def rehint(self):\n        self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH)\n        self.interface.intrinsic.height = at_least(self.interface._MIN_HEIGHT)\n", "path": "cocoa/src/toga_cocoa/widgets/tree.py"}], "after_files": [{"content": "from rubicon.objc import SEL, at, objc_method, objc_property\nfrom travertino.size import at_least\n\nimport toga\nfrom toga_cocoa.libs import (\n    NSBezelBorder,\n    NSIndexSet,\n    NSOutlineView,\n    NSScrollView,\n    NSTableColumn,\n    NSTableViewAnimation,\n    NSTableViewColumnAutoresizingStyle,\n)\nfrom toga_cocoa.widgets.base import Widget\nfrom toga_cocoa.widgets.internal.cells import TogaIconView\nfrom toga_cocoa.widgets.internal.data import TogaData\n\n\ndef node_impl(node):\n    try:\n        return node._impl\n    except AttributeError:\n        node._impl = TogaData.alloc().init()\n        node._impl.attrs = {\"node\": node}\n        return node._impl\n\n\nclass TogaTree(NSOutlineView):\n    interface = objc_property(object, weak=True)\n    impl = objc_property(object, weak=True)\n\n    # OutlineViewDataSource methods\n    @objc_method\n    def outlineView_child_ofItem_(self, tree, child: int, item):\n        # Get the object representing the row\n        if item is None:\n            node = self.interface.data[child]\n        else:\n            node = item.attrs[\"node\"][child]\n\n        # Get the Cocoa implementation for the row. If an _impl\n        # doesn't exist, create a data object for it, and\n        # populate it with initial values for each column.\n\n        return node_impl(node)\n\n    @objc_method\n    def outlineView_isItemExpandable_(self, tree, item) -> bool:\n        return item.attrs[\"node\"].can_have_children()\n\n    @objc_method\n    def outlineView_numberOfChildrenOfItem_(self, tree, item) -> int:\n        if item is None:\n            # How many root elements are there?\n            # If we're starting up, the source may not exist yet.\n            if self.interface.data is not None:\n                return len(self.interface.data)\n            else:\n                return 0\n        else:\n            # How many children does this node have?\n            return len(item.attrs[\"node\"])\n\n    @objc_method\n    def outlineView_viewForTableColumn_item_(self, tree, column, item):\n        col_identifier = str(column.identifier)\n\n        try:\n            value = getattr(item.attrs[\"node\"], col_identifier)\n\n            # if the value is a widget itself, just draw the widget!\n            if isinstance(value, toga.Widget):\n                return value._impl.native\n\n            # Allow for an (icon, value) tuple as the simple case\n            # for encoding an icon in a table cell. Otherwise, look\n            # for an icon attribute.\n            elif isinstance(value, tuple):\n                icon, value = value\n            else:\n                try:\n                    icon = value.icon\n                except AttributeError:\n                    icon = None\n        except AttributeError:\n            # If the node doesn't have a property with the\n            # accessor name, assume an empty string value.\n            value = self.interface.missing_value\n            icon = None\n\n        # creates a NSTableCellView from interface-builder template (does not exist)\n        # or reuses an existing view which is currently not needed for painting\n        # returns None (nil) if both fails\n        identifier = at(f\"CellView_{self.interface.id}\")\n        tcv = self.makeViewWithIdentifier(identifier, owner=self)\n\n        if not tcv:  # there is no existing view to reuse so create a new one\n            # tcv = TogaIconView.alloc().initWithFrame(CGRectMake(0, 0, column.width, 16))\n            tcv = TogaIconView.alloc().init()\n            tcv.identifier = identifier\n\n            # Prevent tcv from being deallocated prematurely when no Python references\n            # are left\n            tcv.retain()\n            tcv.autorelease()\n\n        tcv.setText(str(value))\n        if icon:\n            tcv.setImage(icon._impl.native)\n        else:\n            tcv.setImage(None)\n\n        return tcv\n\n    # 2023-06-29: Commented out this method because it appears to be a\n    # source of significant slowdown when the table has a lot of data\n    # (10k rows). AFAICT, it's only needed if we want custom row heights\n    # for each row. Since we don't currently support custom row heights,\n    # we're paying the cost for no benefit.\n    # @objc_method\n    # def outlineView_heightOfRowByItem_(self, tree, item) -> float:\n    #     default_row_height = self.rowHeight\n\n    #     if item is self:\n    #         return default_row_height\n\n    #     heights = [default_row_height]\n\n    #     for column in self.tableColumns:\n    #         value = getattr(\n    #             item.attrs[\"node\"], str(column.identifier), self.interface.missing_value\n    #         )\n\n    #         if isinstance(value, toga.Widget):\n    #             # if the cell value is a widget, use its height\n    #             heights.append(value._impl.native.intrinsicContentSize().height)\n\n    #     return max(heights)\n\n    @objc_method\n    def outlineView_pasteboardWriterForItem_(\n        self, tree, item\n    ) -> None:  # pragma: no cover\n        # this seems to be required to prevent issue 21562075 in AppKit\n        return None\n\n    # @objc_method\n    # def outlineView_sortDescriptorsDidChange_(self, tableView, oldDescriptors) -> None:\n    #\n    #     for descriptor in self.sortDescriptors[::-1]:\n    #         accessor = descriptor.key\n    #         reverse = descriptor.ascending == 1\n    #         key = self.interface._sort_keys[str(accessor)]\n    #         try:\n    #             self.interface.data.sort(str(accessor), reverse=reverse, key=key)\n    #         except AttributeError:\n    #             pass\n    #         else:\n    #             self.reloadData()\n\n    # OutlineViewDelegate methods\n    @objc_method\n    def outlineViewSelectionDidChange_(self, notification) -> None:\n        self.interface.on_select()\n\n    # target methods\n    @objc_method\n    def onDoubleClick_(self, sender) -> None:\n        node = self.itemAtRow(self.clickedRow).attrs[\"node\"]\n        self.interface.on_activate(node=node)\n\n\nclass Tree(Widget):\n    def create(self):\n        # Create a tree view, and put it in a scroll view.\n        # The scroll view is the _impl, because it's the outer container.\n        self.native = NSScrollView.alloc().init()\n        self.native.hasVerticalScroller = True\n        self.native.hasHorizontalScroller = False\n        self.native.autohidesScrollers = False\n        self.native.borderType = NSBezelBorder\n\n        # Create the Tree widget\n        self.native_tree = TogaTree.alloc().init()\n        self.native_tree.interface = self.interface\n        self.native_tree.impl = self\n        self.native_tree.columnAutoresizingStyle = (\n            NSTableViewColumnAutoresizingStyle.Uniform\n        )\n        self.native_tree.usesAlternatingRowBackgroundColors = True\n        self.native_tree.allowsMultipleSelection = self.interface.multiple_select\n\n        # Create columns for the table\n        self.columns = []\n        if self.interface.headings:\n            for index, (heading, accessor) in enumerate(\n                zip(self.interface.headings, self.interface.accessors)\n            ):\n                self._insert_column(index, heading, accessor)\n        else:\n            self.native_tree.setHeaderView(None)\n            for index, accessor in enumerate(self.interface.accessors):\n                self._insert_column(index, None, accessor)\n\n        # Put the tree arrows in the first column.\n        self.native_tree.outlineTableColumn = self.columns[0]\n\n        self.native_tree.delegate = self.native_tree\n        self.native_tree.dataSource = self.native_tree\n        self.native_tree.target = self.native_tree\n        self.native_tree.doubleAction = SEL(\"onDoubleClick:\")\n\n        # Embed the tree view in the scroll view\n        self.native.documentView = self.native_tree\n\n        # Add the layout constraints\n        self.add_constraints()\n\n    def change_source(self, source):\n        self.native_tree.reloadData()\n\n    def insert(self, parent, index, item):\n        index_set = NSIndexSet.indexSetWithIndex(index)\n        self.native_tree.insertItemsAtIndexes(\n            index_set,\n            inParent=node_impl(parent) if parent else None,\n            withAnimation=NSTableViewAnimation.SlideDown.value,\n        )\n\n    def change(self, item):\n        self.native_tree.reloadItem(node_impl(item))\n\n    def remove(self, parent, index, item):\n        try:\n            index = self.native_tree.childIndexForItem(item._impl)\n            index_set = NSIndexSet.indexSetWithIndex(index)\n            parent = self.native_tree.parentForItem(item._impl)\n            self.native_tree.removeItemsAtIndexes(\n                index_set,\n                inParent=parent,\n                withAnimation=NSTableViewAnimation.SlideUp.value,\n            )\n        except AttributeError:\n            pass\n\n    def clear(self):\n        self.native_tree.reloadData()\n\n    def get_selection(self):\n        if self.interface.multiple_select:\n            selection = []\n\n            current_index = self.native_tree.selectedRowIndexes.firstIndex\n            for i in range(self.native_tree.selectedRowIndexes.count):\n                selection.append(\n                    self.native_tree.itemAtRow(current_index).attrs[\"node\"]\n                )\n                current_index = (\n                    self.native_tree.selectedRowIndexes.indexGreaterThanIndex(\n                        current_index\n                    )\n                )\n\n            return selection\n        else:\n            index = self.native_tree.selectedRow\n            if index != -1:\n                return self.native_tree.itemAtRow(index).attrs[\"node\"]\n            else:\n                return None\n\n    def expand_node(self, node):\n        self.native_tree.expandItem(node_impl(node), expandChildren=True)\n\n    def expand_all(self):\n        self.native_tree.expandItem(None, expandChildren=True)\n\n    def collapse_node(self, node):\n        self.native_tree.collapseItem(node_impl(node), collapseChildren=True)\n\n    def collapse_all(self):\n        self.native_tree.collapseItem(None, collapseChildren=True)\n\n    def _insert_column(self, index, heading, accessor):\n        column = NSTableColumn.alloc().initWithIdentifier(accessor)\n        column.minWidth = 16\n\n        self.columns.insert(index, column)\n        self.native_tree.addTableColumn(column)\n        if index != len(self.columns) - 1:\n            self.native_tree.moveColumn(len(self.columns) - 1, toColumn=index)\n\n        if heading is not None:\n            column.headerCell.stringValue = heading\n\n    def insert_column(self, index, heading, accessor):\n        self._insert_column(index, heading, accessor)\n        self.native_tree.sizeToFit()\n\n    def remove_column(self, index):\n        column = self.columns[index]\n        self.native_tree.removeTableColumn(column)\n\n        # delete column and identifier\n        self.columns.remove(column)\n        self.native_tree.sizeToFit()\n\n    def rehint(self):\n        self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH)\n        self.interface.intrinsic.height = at_least(self.interface._MIN_HEIGHT)\n", "path": "cocoa/src/toga_cocoa/widgets/tree.py"}]}