Repository: beeware/toga

Issue:
Dialog windows are not modal
### Describe the bug

Create a dialog like this:
```
async def on_button_test(widget):
    await self.main_window.info_dialog(title="Dialog", message="An Info Dialog")
```
You can now click on the main window behind the dialog  and the main window is activated although the dialog is still floating above the window. This allows the dialog to be shown again or other actions to be triggered although the user should be blocked in the dialog.

Either change the behavior of dialogs to be modal or add a "modal={True|False}" property to "info_dialog" (and all similar functions) to enforce this.

### Steps to reproduce

1) Create self.main_window.info_dialog
2) Click main window
3) Main window is activated and can be interacted with. 

### Expected behavior

Main window can not be activated (dialog keeps focus)

### Screenshots

_No response_

### Environment

Linux (GenToo). Toga in virtual environment.

### Logs

_No response_

### Additional context

_No response_


Discussion:
Thanks for the report. Looks like this might have been an oversight when we modified the dialog implementation to make it testable. 

It looks like adding `self.native.set_modal(True)` around L33 of `gtk/src/toga_gtk/dialogs.py` will make the dialog modal without breaking the testability of the dialog. There might be a similar change needed for the non-message dialogs (e.g. stack trace and file selection dialogs).
I also just noticed the close button in the default About dialog has no effect (aka not closing the dialog).
I've noticed that as well, but not logged it. I've fixed *that* problem as part of #2244 - the specific change is [this one](https://github.com/beeware/toga/pull/2244/files#diff-49b823fae670b3d17a24b95c5e3037455a0a21cfa2998d6aa031b6003f1bd671R317) - however, that PR is taking a lot longer to land than originally planned. If that change were to slip into a fix for this issue, that would probably be for the better.

Resolving patch:
diff --git a/gtk/src/toga_gtk/dialogs.py b/gtk/src/toga_gtk/dialogs.py
--- a/gtk/src/toga_gtk/dialogs.py
+++ b/gtk/src/toga_gtk/dialogs.py
@@ -30,6 +30,7 @@ def __init__(
             buttons=buttons,
             text=title,
         )
+        self.native.set_modal(True)
         self.build_dialog(**kwargs)
 
         self.native.connect("response", self.gtk_response)


Test patch:
diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py
--- a/android/tests_backend/window.py
+++ b/android/tests_backend/window.py
@@ -87,3 +87,6 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled):
 
     def press_toolbar_button(self, index):
         self.native.onOptionsItemSelected(self._toolbar_items()[index])
+
+    def is_modal_dialog(self, dialog):
+        return True
diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py
--- a/cocoa/tests_backend/window.py
+++ b/cocoa/tests_backend/window.py
@@ -261,3 +261,6 @@ def press_toolbar_button(self, index):
             restype=None,
             argtypes=[objc_id],
         )
+
+    def is_modal_dialog(self, dialog):
+        return True
diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py
--- a/gtk/tests_backend/window.py
+++ b/gtk/tests_backend/window.py
@@ -251,3 +251,6 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled):
     def press_toolbar_button(self, index):
         item = self.impl.native_toolbar.get_nth_item(index)
         item.emit("clicked")
+
+    def is_modal_dialog(self, dialog):
+        return dialog.native.get_modal()
diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py
--- a/iOS/tests_backend/window.py
+++ b/iOS/tests_backend/window.py
@@ -77,3 +77,6 @@ async def close_select_folder_dialog(self, dialog, result, multiple_select):
 
     def has_toolbar(self):
         pytest.skip("Toolbars not implemented on iOS")
+
+    def is_modal_dialog(self, dialog):
+        return True
diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py
--- a/testbed/tests/test_window.py
+++ b/testbed/tests/test_window.py
@@ -560,6 +560,7 @@ async def test_info_dialog(main_window, main_window_probe):
             "Info", "Some info", on_result=on_result_handler
         )
     await main_window_probe.redraw("Info dialog displayed")
+    assert main_window_probe.is_modal_dialog(dialog_result._impl)
     await main_window_probe.close_info_dialog(dialog_result._impl)
     await assert_dialog_result(main_window, dialog_result, on_result_handler, None)
 
diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py
--- a/winforms/tests_backend/window.py
+++ b/winforms/tests_backend/window.py
@@ -148,3 +148,6 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled):
 
     def press_toolbar_button(self, index):
         self._native_toolbar_item(index).OnClick(EventArgs.Empty)
+
+    def is_modal_dialog(self, dialog):
+        return True