wx events end_session and query_end_session are not propagated on macos
Problem When a user wants to shut down his mac, the running erlang application will ignore the shutdown signal and stay open, and this way it will block the macOS shutdown.
Root Cause
The root problem is that :wxFrame.connect(frame, :end_session) in window.ex does not have any effect. And the :end_session event is never delivered to the application. Only :wxFrame.connect(frame, :close_window) is delivered but does not make it possible to differentiate between the user closing a window or the system shutting down.
Current Workaround
Currently, we're checking on receiving the :close_window event whether any window is shown -- if no window is shown, we assume this is a system shutdown and trigger a OS.shutdown() - the problem with this approach is that when a window is open, the window will be closed, but a system shutdown is still being prevented until the user tries again.
Steps to reproduce Open the sample app and open the main window. Now open the macOS activity monitor and use the "Stop" option to stop the application. The application should be stopped -- but actually, only the window is closed, only a second "Stop" call will make the application stop if now window is shown (the workaround is in effect)
Background
When using the macOS "Activity Monitor" or when doing a system shutdown to stop an app macOS is not sending a Unix signal but instead an apple event to the app to close it.
wxWidgets is internally creating two events for this. From the docs:
EVT_QUERY_END_SESSION(func):
Process a wxEVT_QUERY_END_SESSION session event, supplying the member function. This event can be handled in wxApp-derived class only.
EVT_END_SESSION(func):
Process a wxEVT_END_SESSION session event, supplying the member function. This event can be handled in wxApp-derived class only.
Reference: https://github.com/wxWidgets/wxWidgets/blob/master/src/osx/carbon/app.cpp#L209 https://docs.wxwidgets.org/3.0/classwx_close_event.html
Solution Proposal
A) One option to make these events available is to add Connect() calls in wxe_impl.cpp and pipe them through from there (https://github.com/erlang/otp/blob/master/lib/wx/c_src/wxe_impl.cpp#L151)
B) or alternatively make it possible to issue wxEventHandler:connect(:wxApp, :end_session) calls from erlang and trigger the corresponding app Connect() calls. Currently there is no way to reference in :wxApp instance from erlang space so that would be needed to be added.
there might be other options as well.
I notice a related issue on macOS when trying to close the app with Command+Q or from the dock Left Click -> Quit or from the leftmost menu -> Quit.
The app is not closing and in the case of, leftmost menu -> quit, it freezes.
P.S Thank you for the work you did, this project is exactly what I was looking for, for months.
I have found the following:
Create a default MenuBar so that we can intercept the quit command
// fprintf(stderr, "Dummy Close invoked\r\n"); // wxMac really wants a top level window which command-q quits if there are no // windows open, and this will kill the erlang, override default handling
https://github.com/erlang/otp/blob/OTP-24.1.4/lib/wx/c_src/wxe_impl.cpp#L161:L168 https://github.com/erlang/otp/blob/OTP-24.1.4/lib/wx/c_src/wxe_impl.cpp#L210:L214
What worked for me was to subscribe to menu bar events and add an explicit handler for the "quit" menu item (event with id wxID_EXIT aka 5006)
Here is my full test rig for this:
defmodule Example.App do
@moduledoc false
@behaviour :wx_object
# https://github.com/erlang/otp/blob/OTP-24.1.2/lib/wx/include/wx.hrl#L1314
@wx_id_exit 5006
def start_link() do
:wx_object.start_link(__MODULE__, [], [])
end
@impl true
def init(_) do
title = "Example"
size = {400, 400}
wx = :wx.new()
frame = :wxFrame.new(wx, -1, title, size: size)
:wxFrame.show(frame)
# Menubar
menubar = :wxMenuBar.new()
:wxFrame.setMenuBar(frame, menubar)
# App Menu
menu = :wxMenuBar.oSXGetAppleMenu(menubar)
:wxMenu.setTitle(menu, title)
# Remove all items except for Quit since we don't yet handle the standard items
# like "Hide <app>", "Hide Others", "Show All", etc
for item <- :wxMenu.getMenuItems(menu) do
if :wxMenuItem.getId(item) == @wx_id_exit do
:wxMenuItem.setText(item, "Quit #{title}\tCtrl+Q")
else
:wxMenu.delete(menu, item)
end
end
:wxFrame.connect(frame, :command_menu_selected)
state = %{frame: frame}
{frame, state}
end
@impl true
def handle_event({:wx, @wx_id_exit, _, _, _}, state) do
{:stop, :normal, state}
end
end
defmodule Example.Main do
@moduledoc false
def main(_args) do
{:wx_ref, _, _, pid} = Example.App.start_link()
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, _, _, _} ->
:ok
end
end
end
Example.Main.main(System.argv())
That's awesome @wojtekmach I'll have a look at that
@wojtekmach and @mazz-seven I've fixed the MacOS quit button issue in 1.3.2!
But the original issue here is still open :-)