`AsyncShowModal` raises wxAssertionError on Windows
jmkd3v opened this issue · 10 comments
Reproduction
Run the example code at /wxasync/master/src/examples/dialog.py on Windows, but replace AsyncShowDialog
with AsyncShowModal
.
from wx import TextEntryDialog
from wxasync import AsyncShowModal, WxAsyncApp
from asyncio.events import get_event_loop
async def main():
""" This functions demonstrate the use of 'AsyncShowDialog' to Show a
any wx.Dialog asynchronously, and wait for the result.
"""
dlg = TextEntryDialog(None, "Please enter some text:")
return_code = await AsyncShowModal(dlg)
print("The ReturnCode is %s and you entered '%s'" % (return_code, dlg.GetValue()))
app.ExitMainLoop()
if __name__ == '__main__':
app = WxAsyncApp()
loop = get_event_loop()
loop.create_task(main())
loop.run_until_complete(app.MainLoop())
This can also be reproduced in an application with multiple frames. I have attempted to reproduce this on macOS Monterey with no success.
Expected Behavior
Pressing buttons or closing the dialog should cause the window to close and the return_code
should be returned.
Actual Behavior
When pressing buttons or closing the dialog, the following exception is raised:
Task exception was never retrieved
future: <Task finished name='Task-1' coro=<main() done, defined at main.py:6> exception=wxAssertionError('C++ assertion "wxThread::IsMain()" failed at ..\\..\\src\\msw\\evtloop.cpp(176) in wxGUIEventLoop::Dispatch(): only the main thread can process Windows messages')>
Traceback (most recent call last):
File "main.py", line 11, in main
return_code = await AsyncShowModal(dlg)
File "[...]\lib\site-packages\wxasync.py", line 113, in AsyncShowModal
return await loop.run_in_executor(None, dlg.ShowModal)
File "[...]\lib\concurrent\futures\thread.py", line 52, in run
result = self.fn(*self.args, **self.kwargs)
wx._core.wxAssertionError: C++ assertion "wxThread::IsMain()" failed at ..\..\src\msw\evtloop.cpp(176) in wxGUIEventLoop::Dispatch(): only the main thread can process Windows messages
Impact
This issue completely blocks me from using ShowModal in my async application. I must resort to using AsyncShow, which do not block input of other frames.
Hi @jmkd3v, I see the issue. On windows, modal dialogs(using ShowModal) will block the event loop, which is not acceptable for async apps, but it would be possible however to create a modified "AsyncShow" that will disable all the other windows input, not using the os level feature.
@jmkd3v could you have a look at the commit above and the small fix after it. I made it such that there are now "AsyncShowDialog" and "AsyncShowDialogModal".
The disabling of other windows + the SetFocus might need some finetuning. I'd like your feedback about it.
Hi @sirk390 - I tested AsyncShowDialogModal and it appears to work well! I'm wondering if there's a better way to show/hide all of the frames than just looping through and enabling them - what if two dialogs are being shown at once? The frames should only be re-enabled after both dialogs have been closed.
Yes, there is the "Modal inside Modal" case. Maybe it should only Disable all the ancestor windows of the current dialog. That way, if there are mutiple top level windows, only one will be disabled, and "Modal inside Modal" will work correctly.
What is the usual behavior of ShowModal on Windows for the Modal inside Modal case? We should try our best to match that.
It will block input from every other window:
See this small test:
import wx
class Dialog1(wx.Dialog):
def __init__(self, parent=None):
super().__init__(parent, size = (250,150))
self.btn = wx.Button(self, wx.ID_OK, label = "Open Modal")
self.btn2 = wx.Button(self, wx.ID_CANCEL, label = "Close")
self.btn.Bind(wx.EVT_BUTTON, self.OpenModal)
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(self.btn, 1, wx.EXPAND|wx.ALL)
vbox.Add(self.btn2, 1, wx.EXPAND|wx.ALL)
self.SetSizer(vbox)
self.Layout()
def OpenModal(self, event):
dlg = Dialog1()
dlg.ShowModal()
if __name__ == '__main__':
app = wx.App()
dlg = Dialog1()
dlg2 = Dialog1()
dlg2.Show()
dlg.Show()
dlg2.SetPosition((400,50))
app.MainLoop()
However, there might a small issue with the code I committed. It will not restore the initial state. If for some reason the window was disabled? It would enable it.
There is also the case when in the meantime, some windows opened or closed. But this is probably very uncommon so we could not handle this case.
After the fix in my latest commit (to restore previous states instead of Enabling), the async equivalent works exactly the same as the Sync version.
import wx
from wxasync import WxAsyncApp, AsyncBind, StartCoroutine, AsyncShowDialog, AsyncShowDialogModal
from asyncio import get_event_loop
class Dialog1(wx.Dialog):
def __init__(self, parent=None):
super().__init__(parent, size = (250,150))
self.btn = wx.Button(self, wx.ID_OK, label = "Open Modal")
self.btn2 = wx.Button(self, wx.ID_CANCEL, label = "Close")
AsyncBind(wx.EVT_BUTTON, self.OpenModal, self.btn)
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(self.btn, 1, wx.EXPAND|wx.ALL)
vbox.Add(self.btn2, 1, wx.EXPAND|wx.ALL)
self.SetSizer(vbox)
self.Layout()
async def OpenModal(self, event):
dlg = Dialog1()
await AsyncShowDialogModal(dlg)
if __name__ == '__main__':
app = WxAsyncApp()
dlg = Dialog1()
dlg2 = Dialog1()
dlg2.Show()
dlg.Show()
dlg2.SetPosition((400,50))
loop = get_event_loop()
loop.run_until_complete(app.MainLoop())
Alright, awesome. I'll test this again soon and let you know how it looks.
I've uploaded the changes to pip. Let me know if it's working on your side
It is working! I'll close this issue for now and will reopen if I find any issues.