SDL: SDL_EVENT_QUIT when no window nor tray

From 0461180e25b86affeb82e9ff1f97bc22335baa4a Mon Sep 17 00:00:00 2001
From: Semphris <[EMAIL REDACTED]>
Date: Thu, 26 Dec 2024 20:50:46 -0500
Subject: [PATCH] SDL_EVENT_QUIT when no window nor tray

SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE will not fire if there are active tray icons. This impacts only applications that create tray icons, and that at least one icon outlives the last visible top-level window. SDL_EVENT_QUIT will fire when the last active tray is destroyed if there are no active windows.
---
 Android.mk                          |  1 +
 VisualC-GDK/SDL/SDL.vcxproj         |  1 +
 VisualC-GDK/SDL/SDL.vcxproj.filters |  1 +
 VisualC/SDL/SDL.vcxproj             |  1 +
 VisualC/SDL/SDL.vcxproj.filters     |  3 ++
 include/SDL3/SDL_hints.h            |  4 ++
 src/events/SDL_windowevents.c       |  4 +-
 src/tray/SDL_tray_utils.c           | 62 +++++++++++++++++++++++++++++
 src/tray/SDL_tray_utils.h           | 25 ++++++++++++
 src/tray/cocoa/SDL_tray.m           |  5 +++
 src/tray/dummy/SDL_tray.c           |  2 +
 src/tray/unix/SDL_tray.c            | 11 +++++
 src/tray/windows/SDL_tray.c         |  6 +++
 test/testtray.c                     | 52 ++++++++++++++++++++++--
 14 files changed, 173 insertions(+), 5 deletions(-)
 create mode 100644 src/tray/SDL_tray_utils.c
 create mode 100644 src/tray/SDL_tray_utils.h

diff --git a/Android.mk b/Android.mk
index 3e584f9a5b0dc..9e79ee0bc9492 100644
--- a/Android.mk
+++ b/Android.mk
@@ -79,6 +79,7 @@ LOCAL_SRC_FILES := \
 	$(wildcard $(LOCAL_PATH)/src/timer/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/timer/unix/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/tray/dummy/*.c) \
+	$(wildcard $(LOCAL_PATH)/src/tray/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/video/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/video/android/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/video/yuv2rgb/*.c))
diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj
index 2b6437fb7e055..2422201d23f56 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj
+++ b/VisualC-GDK/SDL/SDL.vcxproj
@@ -838,6 +838,7 @@
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Gaming.Xbox.XboxOne.x64'">true</ExcludedFromBuild>
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Gaming.Xbox.XboxOne.x64'">true</ExcludedFromBuild>
     </ClCompile>
+    <ClCompile Include="..\..\src\tray\SDL_tray_utils.c" />
     <ClCompile Include="..\..\src\video\dummy\SDL_nullevents.c" />
     <ClCompile Include="..\..\src\video\dummy\SDL_nullframebuffer.c" />
     <ClCompile Include="..\..\src\video\dummy\SDL_nullvideo.c" />
diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters
index 5ae2609c8042d..7efaf3abe3fef 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj.filters
+++ b/VisualC-GDK/SDL/SDL.vcxproj.filters
@@ -220,6 +220,7 @@
     <ClCompile Include="..\..\src\time\windows\SDL_systime.c" />
     <ClCompile Include="..\..\src\tray\dummy\SDL_tray.c" />
     <ClCompile Include="..\..\src\tray\windows\SDL_tray.c" />
+    <ClCompile Include="..\..\src\tray\SDL_tray_utils.c" />
     <ClCompile Include="..\..\src\video\yuv2rgb\yuv_rgb_lsx.c" />
     <ClCompile Include="..\..\src\video\yuv2rgb\yuv_rgb_sse.c" />
     <ClCompile Include="..\..\src\video\yuv2rgb\yuv_rgb_std.c" />
diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj
index 42d0c13daaec1..e860035ae625a 100644
--- a/VisualC/SDL/SDL.vcxproj
+++ b/VisualC/SDL/SDL.vcxproj
@@ -673,6 +673,7 @@
     <ClCompile Include="..\..\src\time\SDL_time.c" />
     <ClCompile Include="..\..\src\time\windows\SDL_systime.c" />
     <ClCompile Include="..\..\src\tray\windows\SDL_tray.c" />
+    <ClCompile Include="..\..\src\tray\SDL_tray_utils.c" />
     <ClCompile Include="..\..\src\video\dummy\SDL_nullevents.c" />
     <ClCompile Include="..\..\src\video\dummy\SDL_nullframebuffer.c" />
     <ClCompile Include="..\..\src\video\dummy\SDL_nullvideo.c" />
diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters
index d1d24865f57bb..111db56c2079e 100644
--- a/VisualC/SDL/SDL.vcxproj.filters
+++ b/VisualC/SDL/SDL.vcxproj.filters
@@ -1235,6 +1235,9 @@
     <ClCompile Include="..\..\src\tray\windows\SDL_tray.c">
       <Filter>video</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\src\tray\SDL_tray_utils.c">
+      <Filter>video</Filter>
+    </ClCompile>
     <ClCompile Include="..\..\src\video\SDL_RLEaccel.c">
       <Filter>video</Filter>
     </ClCompile>
diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index 1e03207d7d753..028b6e70c1d50 100644
--- a/include/SDL3/SDL_hints.h
+++ b/include/SDL3/SDL_hints.h
@@ -2775,6 +2775,10 @@ extern "C" {
  * - "1": SDL will send a quit event when the last window is requesting to
  *   close. (default)
  *
+ * If there is at least one active system tray icon, SDL_EVENT_QUIT will instead
+ * be sent when both the last window will be closed and the last tray icon will
+ * be destroyed.
+ *
  * This hint can be set anytime.
  *
  * \since This hint is available since SDL 3.1.3.
diff --git a/src/events/SDL_windowevents.c b/src/events/SDL_windowevents.c
index 5535ec5059f9b..5e8d13c6b43e1 100644
--- a/src/events/SDL_windowevents.c
+++ b/src/events/SDL_windowevents.c
@@ -24,7 +24,7 @@
 
 #include "SDL_events_c.h"
 #include "SDL_mouse_c.h"
-
+#include "../tray/SDL_tray_utils.h"
 
 static bool SDLCALL RemoveSupercededWindowEvents(void *userdata, SDL_Event *event)
 {
@@ -247,7 +247,7 @@ bool SDL_SendWindowEvent(SDL_Window *window, SDL_EventType windowevent, int data
         break;
     }
 
-    if (windowevent == SDL_EVENT_WINDOW_CLOSE_REQUESTED && !window->parent) {
+    if (windowevent == SDL_EVENT_WINDOW_CLOSE_REQUESTED && !window->parent && SDL_HasNoActiveTrays()) {
         int toplevel_count = 0;
         SDL_Window *n;
         for (n = SDL_GetVideoDevice()->windows; n; n = n->next) {
diff --git a/src/tray/SDL_tray_utils.c b/src/tray/SDL_tray_utils.c
new file mode 100644
index 0000000000000..aa4b69179b32f
--- /dev/null
+++ b/src/tray/SDL_tray_utils.c
@@ -0,0 +1,62 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#include "../video/SDL_sysvideo.h"
+#include "../events/SDL_events_c.h"
+
+static int active_trays = 0;
+
+extern void SDL_IncrementTrayCount(void)
+{
+    if (++active_trays < 1) {
+        SDL_Log("Active tray count corrupted (%d < 1), this is a bug. The app may close or fail to close unexpectedly.", active_trays);
+    }
+}
+
+extern void SDL_DecrementTrayCount(void)
+{
+    int toplevel_count = 0;
+    SDL_Window *n;
+
+    if (--active_trays < 0) {
+        SDL_Log("Active tray count corrupted (%d < 0), this is a bug. The app may close or fail to close unexpectedly.", active_trays);
+    }
+
+    if (!SDL_GetHintBoolean(SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE, true)) {
+        return;
+    }
+
+    for (n = SDL_GetVideoDevice()->windows; n; n = n->next) {
+        if (!n->parent && !(n->flags & SDL_WINDOW_HIDDEN)) {
+            ++toplevel_count;
+        }
+    }
+
+    if (toplevel_count < 1) {
+        SDL_SendQuit();
+    }
+}
+
+extern bool SDL_HasNoActiveTrays(void)
+{
+    return active_trays < 1;
+}
diff --git a/src/tray/SDL_tray_utils.h b/src/tray/SDL_tray_utils.h
new file mode 100644
index 0000000000000..588752c854db1
--- /dev/null
+++ b/src/tray/SDL_tray_utils.h
@@ -0,0 +1,25 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+extern void SDL_IncrementTrayCount(void);
+extern void SDL_DecrementTrayCount(void);
+extern bool SDL_HasNoActiveTrays(void);
diff --git a/src/tray/cocoa/SDL_tray.m b/src/tray/cocoa/SDL_tray.m
index 5197b7dab893c..843c6c9574377 100644
--- a/src/tray/cocoa/SDL_tray.m
+++ b/src/tray/cocoa/SDL_tray.m
@@ -25,6 +25,7 @@
 
 #include <Cocoa/Cocoa.h>
 
+#include "../SDL_tray_utils.h"
 #include "../../video/SDL_surface_c.h"
 
 /* applicationDockMenu */
@@ -158,6 +159,8 @@ static void DestroySDLMenu(SDL_TrayMenu *menu)
     }
 
 skip_putting_an_icon:
+    SDL_IncrementTrayCount();
+
     return tray;
 }
 
@@ -445,6 +448,8 @@ void SDL_DestroyTray(SDL_Tray *tray)
     }
 
     SDL_free(tray);
+
+    SDL_DecrementTrayCount();
 }
 
 #endif // SDL_PLATFORM_MACOS
diff --git a/src/tray/dummy/SDL_tray.c b/src/tray/dummy/SDL_tray.c
index e8eaebd32640d..7adaed80a1311 100644
--- a/src/tray/dummy/SDL_tray.c
+++ b/src/tray/dummy/SDL_tray.c
@@ -23,6 +23,8 @@
 
 #ifndef SDL_PLATFORM_MACOS
 
+#include "../SDL_tray_utils.h"
+
 SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
 {
     SDL_Unsupported();
diff --git a/src/tray/unix/SDL_tray.c b/src/tray/unix/SDL_tray.c
index b2e81640d9930..7a5ed34ee9b9e 100644
--- a/src/tray/unix/SDL_tray.c
+++ b/src/tray/unix/SDL_tray.c
@@ -21,6 +21,8 @@
 
 #include "SDL_internal.h"
 
+#include "../SDL_tray_utils.h"
+
 #include <dlfcn.h>
 
 /* getpid() */
@@ -52,6 +54,7 @@ typedef enum
     G_CONNECT_SWAPPED = 1 << 1
 } GConnectFlags;
 gulong (*g_signal_connect_data)(gpointer instance, const gchar *detailed_signal, GCallback c_handler, gpointer data, GClosureNotify destroy_data, GConnectFlags connect_flags);
+void (*g_object_unref)(gpointer object);
 
 #define g_signal_connect(instance, detailed_signal, c_handler, data) \
     g_signal_connect_data ((instance), (detailed_signal), (c_handler), (data), NULL, (GConnectFlags) 0)
@@ -237,6 +240,7 @@ static bool init_gtk(void)
     gtk_widget_get_sensitive = dlsym(libgtk, "gtk_widget_get_sensitive");
 
     g_signal_connect_data = dlsym(libgdk, "g_signal_connect_data");
+    g_object_unref = dlsym(libgdk, "g_object_unref");
 
     app_indicator_new = dlsym(libappindicator, "app_indicator_new");
     app_indicator_set_status = dlsym(libappindicator, "app_indicator_set_status");
@@ -257,6 +261,7 @@ static bool init_gtk(void)
         !gtk_menu_shell_insert ||
         !gtk_widget_destroy ||
         !g_signal_connect_data ||
+        !g_object_unref ||
         !app_indicator_new ||
         !app_indicator_set_status ||
         !app_indicator_set_icon ||
@@ -394,6 +399,8 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
 
     app_indicator_set_status(tray->indicator, APP_INDICATOR_STATUS_ACTIVE);
 
+    SDL_IncrementTrayCount();
+
     return tray;
 }
 
@@ -660,5 +667,9 @@ void SDL_DestroyTray(SDL_Tray *tray)
         SDL_RemovePath(tray->icon_path);
     }
 
+    g_object_unref(tray->indicator);
+
     SDL_free(tray);
+
+    SDL_DecrementTrayCount();
 }
diff --git a/src/tray/windows/SDL_tray.c b/src/tray/windows/SDL_tray.c
index 2a623db22e180..ba1a560e4972d 100644
--- a/src/tray/windows/SDL_tray.c
+++ b/src/tray/windows/SDL_tray.c
@@ -21,7 +21,9 @@
 
 #include "SDL_internal.h"
 
+#include "../SDL_tray_utils.h"
 #include "../../core/windows/SDL_windows.h"
+
 #include <windowsx.h>
 #include <shellapi.h>
 #include <stdlib.h> /* FIXME: for mbstowcs_s, wcslen */
@@ -228,6 +230,8 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
 
     SetWindowLongPtr(tray->hwnd, GWLP_USERDATA, (LONG_PTR) tray);
 
+    SDL_IncrementTrayCount();
+
     return tray;
 }
 
@@ -568,4 +572,6 @@ void SDL_DestroyTray(SDL_Tray *tray)
     }
 
     SDL_free(tray);
+
+    SDL_DecrementTrayCount();
 }
diff --git a/test/testtray.c b/test/testtray.c
index df3a5488f6b4a..71533bac7a651 100644
--- a/test/testtray.c
+++ b/test/testtray.c
@@ -9,6 +9,20 @@ static void SDLCALL tray_quit(void *ptr, SDL_TrayEntry *entry)
     SDL_PushEvent(&e);
 }
 
+static bool trays_destroyed = false;
+
+static void SDLCALL tray_close(void *ptr, SDL_TrayEntry *entry)
+{
+    SDL_Tray **trays = (SDL_Tray **) ptr;
+
+    trays_destroyed = true;
+
+    SDL_DestroyTray(trays[0]);
+    SDL_DestroyTray(trays[1]);
+
+    SDL_free(trays);
+}
+
 static void SDLCALL apply_icon(void *ptr, const char * const *filelist, int filter)
 {
     if (!*filelist) {
@@ -500,6 +514,13 @@ int main(int argc, char **argv)
         return 1;
     }
 
+    SDL_Window *w = SDL_CreateWindow("", 640, 480, 0);
+
+    if (!w) {
+        SDL_Log("Couldn't create window: %s", SDL_GetError());
+        goto quit;
+    }
+
     /* TODO: Resource paths? */
     SDL_Surface *icon = SDL_LoadBMP("../test/sdl-test_round.bmp");
 
@@ -517,7 +538,7 @@ int main(int argc, char **argv)
 
     if (!tray) {
         SDL_Log("Couldn't create control tray: %s", SDL_GetError());
-        goto quit;
+        goto clean_window;
     }
 
     SDL_Tray *tray2 = SDL_CreateTray(icon2, "SDL Tray example");
@@ -545,7 +566,20 @@ int main(int argc, char **argv)
     SDL_TrayEntry *entry_quit = SDL_InsertTrayEntryAt(menu, -1, "Quit", SDL_TRAYENTRY_BUTTON);
     CHECK(entry_quit);
 
+    SDL_TrayEntry *entry_close = SDL_InsertTrayEntryAt(menu, -1, "Close", SDL_TRAYENTRY_BUTTON);
+    CHECK(entry_close);
+
+    /* TODO: Track memory! */
+    SDL_Tray **trays = SDL_malloc(sizeof(SDL_Tray *) * 2);
+    if (!trays) {
+        goto clean_all;
+    }
+
+    trays[0] = tray;
+    trays[1] = tray2;
+
     SDL_SetTrayEntryCallback(entry_quit, tray_quit, NULL);
+    SDL_SetTrayEntryCallback(entry_close, tray_close, trays);
 
     SDL_InsertTrayEntryAt(menu, -1, NULL, 0);
 
@@ -582,14 +616,26 @@ int main(int argc, char **argv)
     while (SDL_WaitEvent(&e)) {
         if (e.type == SDL_EVENT_QUIT) {
             break;
+        } else if (e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
+            SDL_DestroyWindow(w);
+            w = NULL;
         }
     }
 
 clean_all:
-    SDL_DestroyTray(tray2);
+    if (!trays_destroyed) {
+        SDL_DestroyTray(tray2);
+    }
 
 clean_tray1:
-    SDL_DestroyTray(tray);
+    if (!trays_destroyed) {
+        SDL_DestroyTray(tray);
+    }
+
+clean_window:
+    if (w) {
+        SDL_DestroyWindow(w);
+    }
 
 quit:
     SDL_Quit();