enthought / traitsui

TraitsUI: Traits-capable windowing framework
http://docs.enthought.com/traitsui
Other
296 stars 95 forks source link

Qt Application does not exit when the last UI is closed manually #1442

Open kitchoi opened 3 years ago

kitchoi commented 3 years ago

This issue follows a report from this thread in the user mailing list.

To reproduce:

from traits.api import HasTraits, Float
from traitsui.api import Item, View
from traitsui.menu import MenuBar, Menu, Action, CloseAction

class MyApp(HasTraits):
    a = Float(1.0)
    menubar = MenuBar(
        Menu(
            CloseAction,
            name='File'
        )
    )
    traits_view = View(
        Item('a'),
        menubar=menubar
    )

app = MyApp()
app.configure_traits()

Attempt to close the window by pressing the "Close" item in the menu does make the window invisible, but the Python process is still blocking. The expectation is that it should behave like the "OK" button and the Python process should exit.

I also came across this issue when I experimented with defining a button that does the same action as the OK button (motivated by https://github.com/enthought/traitsui/issues/1148), by calling ui.owner.close() where ui is the main UI instance created by configure_traits (or edit_traits).

Quote from the mailing list > I came across what looks like another (annoying) bug in traitsui/pyface 7.0 with PyQt5 5.14, this time with the menu CloseAction. (This is under Python 3.6.10 on RHEL 7.7.) If I try to close the main window using the CloseAction item in a menu in the menubar, Qt hangs in an infinite loop after destroying the window. Closing the window by clicking on the window's "X" close icon works just fine, with a message in the shell from which Python was executed indicating the python process has been killed. Following is a super-short example that reproduces the problem: > ``` > > from traits.api import HasTraits, Float > from traitsui.api import Item, View > from traitsui.menu import MenuBar, Menu, Action, CloseAction > > class MyApp(HasTraits): > a = Float(1.0) > menubar = MenuBar( > Menu( > CloseAction, > name='File' > ) > ) > traits_view = View( > Item('a'), > menubar=menubar > ) > > app = MyApp() > app.configure_traits() > ``` > > By adding a custom handler to this code to override the Handler.close method and insert a "import ipdb; ipdb.set_trace()" statement, then stepping, it appears the infinite loop occurs after exiting traitsui.qt4.toolkit._KeyEventHook.eventFilter a number of times. Each time that method gets called, the "event" argument is a different QEvent, with a type such as 25 (WindowDeactivate), 52 (DeferredDelete), 71 (ChildRemoved), and so on. Eventually, it gets called with a QEvent type of 16, which does not appear to be a defined QEvent; after returning from this call, even with an ipdb step command, Python hangs. > > Perhaps this is a bug upstream in PyQt5 instead of PyFace or traitsui, but I wouldn't know how to determine this. Hopefully a developer would like to look at it, and if it is a bug, report this as such for the appropriate package? > > Jean-Paul Davis > >
kitchoi commented 3 years ago

From my preliminary investigation, when the close action is invoked, this method is called: https://github.com/enthought/traitsui/blob/464b20200eef30f23317ffd3f6a125520329f7e2/traitsui/qt4/ui_live.py#L227-L232

But in order to tell Qt that the window is being closed, this function needs to be called, but is never called: https://github.com/enthought/traitsui/blob/464b20200eef30f23317ffd3f6a125520329f7e2/traitsui/qt4/ui_live.py#L234-L242

I also checked that QApplication.quitOnLastWindowClosed() returns True, so we expect the Qt application to exit when the last window is closed.

kitchoi commented 3 years ago

But in order to tell Qt that the window is being closed, this function needs to be called, but is never called:~

Scratch that. I am not sure about this any more. This method is hooked up to the window control finished signal, but that signal has not been emitted.

However, this method is called: https://github.com/enthought/traitsui/blob/464b20200eef30f23317ffd3f6a125520329f7e2/traitsui/qt4/ui_base.py#L276-L280

The invocation of UI.dispose is what caused the window to disappear. UI.dispose schedules the window to be destroyed. I think this is different from closing the widget, and the close event may not be emitted (?). Note that in the context of UI.dispose, the control may not be a top level window any more. The BaseDialog has that context, but not UI.

corranwebster commented 3 years ago

Investigating, if you run with kind="livemodal" or "modal" then things work as expected.

That makes me a bit suspicious of this code: https://github.com/enthought/traitsui/blob/e913f98d10e96624245c603d856b33ded665d565/traitsui/qt4/view_application.py#L129-L131

This is the event loop that is not exiting, I think.

aaronayres35 commented 3 years ago

ref: https://github.com/enthought/chaco/issues/716

EytanDn commented 11 months ago

Having the same issue after migrating my app from PyQt6 to PySide6. Any updates on this?

corranwebster commented 11 months ago

This is an old issue, and I haven't seen this behaviour in a while. There were issues with older versions of PySide 6 not exiting cleanly (this was a PySide issue, not a TraitsUI issue ultimately), but that has been fixed in PySide 6.4.3 or later.

Which versions of TraitsUI and PySide are you seeing this on?