sirk390 / wxasync

asyncio support for wxpython
MIT License
76 stars 9 forks source link

`AsyncShowModal` raises wxAssertionError on Windows #16

Closed jmkd3v closed 2 years ago

jmkd3v commented 2 years ago

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.

sirk390 commented 2 years ago

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.

sirk390 commented 2 years ago

@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.

jmkd3v commented 2 years ago

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.

sirk390 commented 2 years ago

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.

jmkd3v commented 2 years ago

What is the usual behavior of ShowModal on Windows for the Modal inside Modal case? We should try our best to match that.

sirk390 commented 2 years ago

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.

sirk390 commented 2 years ago

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())
jmkd3v commented 2 years ago

Alright, awesome. I'll test this again soon and let you know how it looks.

sirk390 commented 2 years ago

I've uploaded the changes to pip. Let me know if it's working on your side

jmkd3v commented 2 years ago

It is working! I'll close this issue for now and will reopen if I find any issues.