Nested Modal Dialogs - Looks like Keyboard Events sometimes get incorrectly routed

Hi,

in the application, which I’m currently developing (as part of my work), the nested modal
dialogs are used in order to navigate linked data entries.

We have started using the application in production, and during the usage, the application gets crashes several time per day with following C++ assertion failure:

wx._core.wxAssertionError: C++ assertion ""IsRunning()"" failed at ..\..\src\common\evtloopcmn.cpp(92) in wxEventLoopBase::Exit(): Use ScheduleExit() on not running loop

(Under nested Modal Dialog I mean a modal dialog, which is called from event handler of another modal dialog)

This failure is happening inside on CharHook handler with following code:

def OnHookHandler(evt: wx.KeyEvent, dialog: wx.Dialog) -> None:
    if evt.GetKeyCode() == wx.WXK_ESCAPE:
        print(f"Trying to close {dialog=}")
        dialog.EndModal(wx.ID_CANCEL)
    else:
        evt.Skip()

which is bind to the handle EVT_CHAR_HOOK inside dialog as follows:

self.Bind(wx.EVT_CHAR_HOOK, partial(OnHookHandler, dialog=self))

I should mention that this exception happens not every time, when dialog get closed, but at least several times per day during intensive usage of the application.

When looking at logs, I’m able to observe, that in case of crashes, OnHookHandler for the modal dialog, which was opened first (which should be in background) is called before hook handler of modal dialog, which is currently active, which, in turn, leads to the described C++ assertion failure.

So, my assumption is, that ESC handler for the “background” modal dialog is getting called, although other modal dialog is currently active

The version of the wxPython is 4.1.1

Is there a way to prevent such failures from happening?

Many thanks in advance

Serhiy Yevtushenko

Have you tried calling self.Unbind() in the parent dialog (using the same args as in the original self.Bind() call) just before calling the nested dialog’s ShowModal() method, and then calling self.Bind() again afterwards?

Hi, Richard

Thank you for your suggestion. I will definetly try to implement it. I have a question concerning passing the same arguments to Unbind call. As you have seen in my example, I am using partial wrapper from functional python built-in package, in order to use same handler in different places. So, my question is - should I store first the result of partial call in some variable and pass it to unbind, or passing second time partial wrapper will be enough (as I am not sure, whether
partial(some_function, "some_argument")==partial(some_function, "some_argument")
)?

Actually I’m now not sure that you do need to pass all of the same arguments. I said that originally because that’s what I do in all the examples where I call Unbind() in my own code. However, my code doesn’t use partial() so I’ve not had that complication.

I’ve now had a look at other examples elsewhere and they only include the event binder (e.g. wx.EVT_CHAR_HOOK) and, optionally, the source of the event if the source is a child of the object that did the Bind().

I therefore think for your case self.Unbind(wx.EVT_CHAR_HOOK) should be sufficient.

I have created a simple unit test to check, whether functions, wrapped by partial, having the same arguments, will be equal between themselves.

def test_partial_inequality():
    def function(arg_1, arg_2):
        print(f"function {arg_1} {arg_2}")

    assert partial(function, arg_2="one") != partial(function, arg_2="one")

It has shown that this is not the case, so, I ended up storing the argument of call to bind as a variable.

according to the docu nested modal dialogs will create nested event loops and only the top one is active, so doing something you are trying (and I haven’t got the faintest idea what sense it could make) you’ll have to sort out the loops !!! (I think :rofl:)

Hi, Georg,

thank you for your comment.

The event loops (and nested event loop in the case of nested modal dialogs boxes) are being managed in the wxWidgets C++ based backend code.

According to my (limited) knowledge, in the wxPython python code one is not able to manage event loops.
Therefore, the suggestion of Richard presented (in my opinion) a realistic option to work around this issue.

In theory, such an exception should not occur at all, but in practice the management of event loops includes multithreading code which is inherently hard, and I according to the information that I get from logs functions incorrectly in some cases.

Kind Regards
Serhiy

you don’t specify a source in the Bind and I have experienced that binding with source ‘improves’ the routing :face_with_hand_over_mouth:

however, if threading turns into a shredding, then best is to avoid it (and that way all dialogs could be closed with a single Esc, if need for speed :hot_face:)

import wx

class CatchEsc(wx.EventFilter):
    def __init__(self, dlg):
        super().__init__()
        self.char_hook = wx.EVT_CHAR_HOOK._getEvtType()
        self.dlg = dlg

    def FilterEvent(self, evt):
        if evt.GetEventType() == self.char_hook and\
                evt.GetKeyCode() == wx.WXK_ESCAPE:
            if dlg := self.dlg.__class__.curr_dlg:
                dlg.evt_quit(None)
            return self.Event_Ignore
        return self.Event_Skip

class Gui(wx.Frame):

    def __init__(self, parent):
        super().__init__(parent, title='nested modal dialogs')

        btn = wx.Button(self, label='left click me, please')
        btn.Bind(wx.EVT_BUTTON, self.test)

        self.Centre()
        self.Show()

    def test(self, _):
        dlg = Nested(self, None)
        dlg.SetTitle(f'curr {id(dlg)}')
        evf = CatchEsc(dlg)
        self.AddFilter(evf)
        dlg.ShowModal()
        self.RemoveFilter(evf)

class Nested(wx.Dialog):

    curr_dlg = None

    def __init__(self, parent, prev):
        super().__init__(parent)
        print(f'{id(self)} init')
        __class__.curr_dlg = self
        self.prev = prev

        btn = wx.Button(self, label='left click for next dialog')
        btn.Bind(wx.EVT_BUTTON, self.next)
        self.Bind(wx.EVT_CLOSE, self.evt_quit)
        self.Bind(wx.EVT_WINDOW_DESTROY,
                    lambda _: print(f'{id(self)} detroyed'), self)

    def next(self, _):
        dlg = Nested(self, self)
        __class__.curr_dlg = dlg
        dlg.SetTitle(f'prev {id(dlg.prev)}  curr {id(dlg)}')
        dlg.ShowModal()

    def evt_quit(self, _):
        dlg = __class__.curr_dlg
        dlg.Destroy()
        __class__.curr_dlg = self.prev

app = wx.App()
Gui(None)
app.MainLoop()

Hi, Georg.

You suggestions look interesting.

Could you please clarify on following points:

  1. I assume that source argument in bind should be the dialog window or component, to which bind is performed (i.e., button, for example)
    Is it so? Could you clarify, what will be the difference between following options:

Option A (using source):

   buttonId = wx.NewIdRef() 
   btn = wx.Button(self, label="Some Action", id=buttonId)
   self.Bind(wx.EVT_BUTTON, handler, source=btn)

Option B (using id)

   buttonId = wx.NewIdRef() 
   btn = wx.Button(self, label="Some Action", id=buttonId)
   self.Bind(wx.EVT_BUTTON, handler, id=buttonId)

Option C (binding on component):

   btn = wx.Button(self, label="Some Action")
   btn.Bind(wx.EVT_BUTTON, handler)

Option D (binding on component and using id):

   buttonId = wx.NewIdRef() 
   btn = wx.Button(self, label="Some Action", id=buttonId)
   btn.Bind(wx.EVT_BUTTON, handler, id=buttonId)
  1. In the example with event filter, you use binding on button, and do not use source argument of Bind? Are there any particular design rationale for doing so?

Many thanks in advance

Serhiy Yevtushenko

in a real world I would not use the EventFilter because I strongly believe the local binding of EVT_CHAR_HOOK works, but your closing (especially Destroying) is faulty

another point is the design of what problem (I still cannot imagine the use case): if the nesting is deep enough it looks like wx will stop creating new dialogs

however, binding keeps being interesting (I think :woozy_face:)

Hi, George.

What problem is being solved:

I get a three-level catalog (with first level entries having up to more than 100 thousands entries), entries in which are getting processed using grids. The nesting of dialog boxes gets up to threelevels (two levels of catalog + one level for additional details), but basically it is a way to drill down on data on the next level.
The first level of data is being handled by the wx.grid in notebook page, second and third - by grids in nested dialog boxes.

I’m interested in your statement:
" but your closing (especially Destroying ) is faulty".
Could you clarify, which closing, in your opinion, would be non-faulty for modal dialogs?

well, that gives some light on the problem :smiley:
first, the exception seems to happen at ‘closing’ the dialog and you simply say it’s the event rooting & I suggest your destroying may not be faultless
second, if the depth is only 3 the event filter could be an option, at least as a test (easily implemented), because the rooting is then entirely yours (of course if the docu is correct)
third only the blocking seems to be the motivation for using dialogs and that could possibly be done with ordinary frames (at least that would be another way to narrow down the problem)

P.S. to avoid the race that the current dialog is still winding down (high workload) but the previous already catches an Esc, simply ignore that by asking if dialog.IsActive()