Repository: beeware/toga

Issue:
Adding a window with content to an app raises an error about duplicate widget IDs
If you create a window, and add widgets to that window, then add the window to an app, the widgets are reported as duplicated content by the app registry.

**To Reproduce**
Steps to reproduce the behavior:
1. Create a new window that has content
2. Add the window to an app

You'll get an error about duplicated IDs in the app. 

**Expected behavior**

Adding a window to an app shouldn't raise duplicate ID errors.

**Environment:**
 - Operating System:
 - Python version:
 - Software versions:
   - Briefcase:
   - Toga: 0.3.0dev39
   - ...

**Additional context**

#1599 added the widget registry; the error appears to be in that implementation.




Discussion:

Resolving patch:
diff --git a/src/cocoa/src/toga_cocoa/window.py b/src/cocoa/src/toga_cocoa/window.py
--- a/src/cocoa/src/toga_cocoa/window.py
+++ b/src/cocoa/src/toga_cocoa/window.py
@@ -139,6 +139,11 @@ def onToolbarButtonPress_(self, obj) -> None:
         item.action(obj)
 
 
+class TogaWindow(NSWindow):
+    interface = objc_property(object, weak=True)
+    impl = objc_property(object, weak=True)
+
+
 class Window:
     def __init__(self, interface, title, position, size):
         self.interface = interface
@@ -156,12 +161,14 @@ def __init__(self, interface, title, position, size):
 
         # Create the window with a default frame;
         # we'll update size and position later.
-        self.native = NSWindow.alloc().initWithContentRect(
+        self.native = TogaWindow.alloc().initWithContentRect(
             NSMakeRect(0, 0, 0, 0),
             styleMask=mask,
             backing=NSBackingStoreBuffered,
             defer=False,
         )
+        self.native.interface = self.interface
+        self.native.impl = self
 
         self.set_title(title)
         self.set_size(size)
diff --git a/src/core/src/toga/window.py b/src/core/src/toga/window.py
--- a/src/core/src/toga/window.py
+++ b/src/core/src/toga/window.py
@@ -104,7 +104,6 @@ def app(self, app):
 
         self._app = app
         self._impl.set_app(app._impl)
-        app.widgets.update(self.widgets)
 
         if self.content:
             self.content.app = app
diff --git a/src/dummy/src/toga_dummy/window.py b/src/dummy/src/toga_dummy/window.py
--- a/src/dummy/src/toga_dummy/window.py
+++ b/src/dummy/src/toga_dummy/window.py
@@ -1,6 +1,22 @@
 from .utils import LoggedObject, not_required, not_required_on
 
 
+@not_required
+class Viewport:
+    def __init__(self, window):
+        self.baseline_dpi = 96
+        self.dpi = 96
+        self.window = window
+
+    @property
+    def width(self):
+        return self.window.get_size()[0]
+
+    @property
+    def height(self):
+        return self.window.get_size()[1]
+
+
 class Window(LoggedObject):
     def __init__(self, interface, title, position, size):
         super().__init__()
@@ -18,6 +34,8 @@ def clear_content(self):
 
     def set_content(self, widget):
         self._action("set content", widget=widget)
+        self._set_value("content", widget)
+        widget.viewport = Viewport(self)
 
     def get_title(self):
         return self._get_value("title")


Test patch:
diff --git a/src/core/tests/test_window.py b/src/core/tests/test_window.py
--- a/src/core/tests/test_window.py
+++ b/src/core/tests/test_window.py
@@ -124,19 +124,32 @@ def test_set_app_after_content(self):
 
     def test_set_app_adds_window_widgets_to_app(self):
 
-        id1, id2, id3 = "id1", "id2", "id3"
+        id0, id1, id2, id3 = "id0", "id1", "id2", "id3"
         widget1, widget2, widget3 = (
-            toga.Widget(id=id1),
-            toga.Widget(id=id2),
-            toga.Widget(id=id3),
+            toga.Label(id=id1, text="label 1"),
+            toga.Label(id=id2, text="label 1"),
+            toga.Label(id=id3, text="label 1"),
         )
-        self.window.widgets.update({widget1, widget2, widget3})
+        content = toga.Box(id=id0, children=[widget1, widget2, widget3])
 
+        self.window.content = content
+
+        # The window has widgets in it's repository
+        self.assertEqual(len(self.window.widgets), 4)
+        self.assertEqual(self.window.widgets[id0], content)
+        self.assertEqual(self.window.widgets[id1], widget1)
+        self.assertEqual(self.window.widgets[id2], widget2)
+        self.assertEqual(self.window.widgets[id3], widget3)
+
+        # The app doesn't know about the widgets
         self.assertEqual(len(self.app.widgets), 0)
 
+        # Assign the window to the app
         self.window.app = self.app
 
-        self.assertEqual(len(self.app.widgets), 3)
+        # The window's content widgets are now known to the app.
+        self.assertEqual(len(self.app.widgets), 4)
+        self.assertEqual(self.app.widgets[id0], content)
         self.assertEqual(self.app.widgets[id1], widget1)
         self.assertEqual(self.app.widgets[id2], widget2)
         self.assertEqual(self.app.widgets[id3], widget3)