From 01b9b0edb7fe67d333cc26744a525c9ee0fd578e Mon Sep 17 00:00:00 2001 From: Semphriss <66701383+Semphriss@users.noreply.github.com> Date: Tue, 24 Dec 2024 13:36:39 -0500 Subject: [PATCH] Add system tray support (#10873) --- Android.mk | 1 + CMakeLists.txt | 17 + VisualC-GDK/SDL/SDL.vcxproj | 14 +- VisualC-GDK/SDL/SDL.vcxproj.filters | 4 + VisualC/SDL/SDL.vcxproj | 3 + VisualC/SDL/SDL.vcxproj.filters | 9 + include/SDL3/SDL.h | 1 + include/SDL3/SDL_tray.h | 431 +++++++++++++++++ src/dynapi/SDL_dynapi.sym | 21 + src/dynapi/SDL_dynapi_overrides.h | 21 + src/dynapi/SDL_dynapi_procs.h | 21 + src/tray/cocoa/SDL_tray.m | 458 ++++++++++++++++++ src/tray/dummy/SDL_tray.c | 139 ++++++ src/tray/unix/SDL_tray.c | 664 ++++++++++++++++++++++++++ src/tray/windows/SDL_tray.c | 589 +++++++++++++++++++++++ src/video/windows/SDL_surface_utils.c | 95 ++++ src/video/windows/SDL_surface_utils.h | 38 ++ test/CMakeLists.txt | 1 + test/sdl-test_round.bmp | Bin 0 -> 147594 bytes test/testtray.c | 599 +++++++++++++++++++++++ 20 files changed, 3125 insertions(+), 1 deletion(-) create mode 100644 include/SDL3/SDL_tray.h create mode 100644 src/tray/cocoa/SDL_tray.m create mode 100644 src/tray/dummy/SDL_tray.c create mode 100644 src/tray/unix/SDL_tray.c create mode 100644 src/tray/windows/SDL_tray.c create mode 100644 src/video/windows/SDL_surface_utils.c create mode 100644 src/video/windows/SDL_surface_utils.h create mode 100644 test/sdl-test_round.bmp create mode 100644 test/testtray.c diff --git a/Android.mk b/Android.mk index 56c817a2b6..3e584f9a5b 100644 --- a/Android.mk +++ b/Android.mk @@ -78,6 +78,7 @@ LOCAL_SRC_FILES := \ $(wildcard $(LOCAL_PATH)/src/time/unix/*.c) \ $(wildcard $(LOCAL_PATH)/src/timer/*.c) \ $(wildcard $(LOCAL_PATH)/src/timer/unix/*.c) \ + $(wildcard $(LOCAL_PATH)/src/tray/dummy/*.c) \ $(wildcard $(LOCAL_PATH)/src/video/*.c) \ $(wildcard $(LOCAL_PATH)/src/video/android/*.c) \ $(wildcard $(LOCAL_PATH)/src/video/yuv2rgb/*.c)) diff --git a/CMakeLists.txt b/CMakeLists.txt index 695b6ed665..5a6cad0280 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1567,6 +1567,9 @@ elseif(UNIX AND NOT APPLE AND NOT RISCOS AND NOT HAIKU) CheckVivante() CheckVulkan() CheckQNXScreen() + + sdl_glob_sources("${SDL3_SOURCE_DIR}/src/tray/unix/*.c") + set(HAVE_SDL_TRAY TRUE) endif() if(UNIX) @@ -2075,6 +2078,9 @@ elseif(WINDOWS) set(HAVE_RENDER_VULKAN TRUE) endif() endif() + + sdl_glob_sources("${SDL3_SOURCE_DIR}/src/tray/windows/*.c") + set(HAVE_SDL_TRAY TRUE) endif() if(SDL_HIDAPI) @@ -2351,6 +2357,11 @@ elseif(APPLE) endif() endif() endif() + + if(MACOS) + sdl_glob_sources("${SDL3_SOURCE_DIR}/src/tray/cocoa/*.m") + set(HAVE_SDL_TRAY TRUE) + endif() endif() # Minimum version for $ @@ -2973,6 +2984,8 @@ if(SDL_VIDEO) endif() endif() +sdl_glob_sources(${SDL3_SOURCE_DIR}/src/tray/*.c) + if(SDL_GPU) if(HAVE_D3D11_H) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/gpu/d3d11/*.c") @@ -3055,6 +3068,10 @@ if(NOT HAVE_SDL_PROCESS) set(SDL_PROCESS_DUMMY 1) sdl_glob_sources(${SDL3_SOURCE_DIR}/src/process/dummy/*.c) endif() +if(NOT HAVE_SDL_TRAY) + set(SDL_TRAY_DUMMY 1) + sdl_glob_sources(${SDL3_SOURCE_DIR}/src/tray/dummy/*.c) +endif() if(NOT HAVE_CAMERA) set(SDL_CAMERA_DRIVER_DUMMY 1) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/camera/dummy/*.c") diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj index 008af3dff8..2b6437fb7e 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj +++ b/VisualC-GDK/SDL/SDL.vcxproj @@ -592,6 +592,7 @@ + @@ -827,6 +828,16 @@ + + true + true + + + true + true + true + true + @@ -855,6 +866,7 @@ + @@ -889,4 +901,4 @@ - \ No newline at end of file + diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters index 230c95d2c2..5ae2609c80 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj.filters +++ b/VisualC-GDK/SDL/SDL.vcxproj.filters @@ -181,6 +181,7 @@ + @@ -217,6 +218,8 @@ + + @@ -435,6 +438,7 @@ + diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj index 306a883d04..42d0c13daa 100644 --- a/VisualC/SDL/SDL.vcxproj +++ b/VisualC/SDL/SDL.vcxproj @@ -489,6 +489,7 @@ + @@ -671,6 +672,7 @@ + @@ -701,6 +703,7 @@ + diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters index aa86194fab..d1d24865f5 100644 --- a/VisualC/SDL/SDL.vcxproj.filters +++ b/VisualC/SDL/SDL.vcxproj.filters @@ -690,6 +690,9 @@ video\yuv2rgb + + video\windows + video\windows @@ -1229,6 +1232,9 @@ time\windows + + video + video @@ -1301,6 +1307,9 @@ video\dummy + + video\windows + video\windows diff --git a/include/SDL3/SDL.h b/include/SDL3/SDL.h index e36c67b6db..0b4388eb5c 100644 --- a/include/SDL3/SDL.h +++ b/include/SDL3/SDL.h @@ -81,6 +81,7 @@ #include #include #include +#include #include #include #include diff --git a/include/SDL3/SDL_tray.h b/include/SDL3/SDL_tray.h new file mode 100644 index 0000000000..b7c5020706 --- /dev/null +++ b/include/SDL3/SDL_tray.h @@ -0,0 +1,431 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + 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. +*/ + +/** + * # CategoryTray + * + * System tray menu support. + */ + +#ifndef SDL_tray_h_ +#define SDL_tray_h_ + +#include + +#include + +#include +/* Set up for C function definitions, even when using C++ */ +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct SDL_Tray SDL_Tray; +typedef struct SDL_TrayMenu SDL_TrayMenu; +typedef struct SDL_TrayEntry SDL_TrayEntry; + +/** + * Flags that control the creation of system tray entries. + * + * Some of these flags are required; exactly one of them must be specified at + * the time a tray entry is created. Other flags are optional; zero or more of + * those can be OR'ed together with the required flag. + * + * \since This datatype is available since SDL 3.0.0. + * + * \sa SDL_InsertTrayEntryAt + */ +typedef Uint32 SDL_TrayEntryFlags; + +#define SDL_TRAYENTRY_BUTTON 0x00000001u /**< Make the entry a simple button. Required. */ +#define SDL_TRAYENTRY_CHECKBOX 0x00000002u /**< Make the entry a checkbox. Required. */ +#define SDL_TRAYENTRY_SUBMENU 0x00000004u /**< Prepare the entry to have a submenu. Required */ +#define SDL_TRAYENTRY_DISABLED 0x80000000u /**< Make the entry disabled. Optional. */ +#define SDL_TRAYENTRY_CHECKED 0x40000000u /**< Make the entry checked. This is valid only for checkboxes. Optional. */ + +/** + * A callback that is invoked when a tray entry is selected. + * + * \param userdata an optional pointer to pass extra data to the callback when + * it will be invoked. + * \param entry the tray entry that was selected. + * + * \sa SDL_SetTrayEntryCallback + */ +typedef void (SDLCALL *SDL_TrayCallback)(void *userdata, SDL_TrayEntry *entry); + +/** + * Create an icon to be placed in the operating system's tray, or equivalent. + * + * Many platforms advise not using a system tray unless persistence is a + * necessary feature. Avoid needlessly creating a tray icon, as the user may + * feel like it clutters their interface. + * + * Using tray icons require the video subsystem. + * + * \param icon a surface to be used as icon. May be NULL. + * \param tooltip a tooltip to be displayed when the mouse hovers the icon. Not + * supported on all platforms. May be NULL. + * \returns The newly created system tray icon. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTrayMenu + * \sa SDL_GetTrayMenu + * \sa SDL_DestroyTray + */ +extern SDL_DECLSPEC SDL_Tray *SDLCALL SDL_CreateTray(SDL_Surface *icon, const char *tooltip); + +/** + * Updates the system tray icon's icon. + * + * \param tray the tray icon to be updated. + * \param icon the new icon. May be NULL. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTray + */ +extern SDL_DECLSPEC void SDLCALL SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon); + +/** + * Updates the system tray icon's tooltip. + * + * \param tray the tray icon to be updated. + * \param tooltip the new tooltip. May be NULL. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTray + */ +extern SDL_DECLSPEC void SDLCALL SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip); + +/** + * Create a menu for a system tray. + * + * This should be called at most once per tray icon. + * + * This function does the same thing as SDL_CreateTraySubmenu(), except that it + * takes a SDL_Tray instead of a SDL_TrayEntry. + * + * A menu does not need to be destroyed; it will be destroyed with the tray. + * + * \param tray the tray to bind the menu to. + * \returns the newly created menu. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTray + * \sa SDL_GetTrayMenu + * \sa SDL_GetTrayMenuParentTray + */ +extern SDL_DECLSPEC SDL_TrayMenu *SDLCALL SDL_CreateTrayMenu(SDL_Tray *tray); + +/** + * Create a submenu for a system tray entry. + * + * This should be called at most once per tray entry. + * + * This function does the same thing as SDL_CreateTrayMenu, except that it + * takes a SDL_TrayEntry instead of a SDL_Tray. + * + * A menu does not need to be destroyed; it will be destroyed with the tray. + * + * \param entry the tray entry to bind the menu to. + * \returns the newly created menu. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_InsertTrayEntryAt + * \sa SDL_GetTraySubmenu + * \sa SDL_GetTrayMenuParentEntry + */ +extern SDL_DECLSPEC SDL_TrayMenu *SDLCALL SDL_CreateTraySubmenu(SDL_TrayEntry *entry); + +/** + * Gets a previously created tray menu. + * + * You should have called SDL_CreateTrayMenu() on the tray object. This + * function allows you to fetch it again later. + * + * This function does the same thing as SDL_GetTraySubmenu(), except that it + * takes a SDL_Tray instead of a SDL_TrayEntry. + * + * A menu does not need to be destroyed; it will be destroyed with the tray. + * + * \param tray the tray entry to bind the menu to. + * \returns the newly created menu. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTray + * \sa SDL_CreateTrayMenu + */ +extern SDL_DECLSPEC SDL_TrayMenu *SDLCALL SDL_GetTrayMenu(SDL_Tray *tray); + +/** + * Gets a previously created tray entry submenu. + * + * You should have called SDL_CreateTraySubenu() on the entry object. This + * function allows you to fetch it again later. + * + * This function does the same thing as SDL_GetTrayMenu(), except that it + * takes a SDL_TrayEntry instead of a SDL_Tray. + * + * A menu does not need to be destroyed; it will be destroyed with the tray. + * + * \param entry the tray entry to bind the menu to. + * \returns the newly created menu. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_InsertTrayEntryAt + * \sa SDL_CreateTraySubmenu + */ +extern SDL_DECLSPEC SDL_TrayMenu *SDLCALL SDL_GetTraySubmenu(SDL_TrayEntry *entry); + +/** + * Returns a list of entries in the menu, in order. + * + * \param menu The menu to get entries from. + * \param size An optional pointer to obtain the number of entries in the menu. + * \returns the entries within the given menu. The pointer becomes invalid when + * any function that inserts or deletes entries in the menu is called. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_RemoveTrayEntry + * \sa SDL_InsertTrayEntryAt + */ +extern SDL_DECLSPEC const SDL_TrayEntry **SDLCALL SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size); + +/** + * Removes a tray entry. + * + * \param entry The entry to be deleted. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + */ +extern SDL_DECLSPEC void SDLCALL SDL_RemoveTrayEntry(SDL_TrayEntry *entry); + +/** + * Insert a tray entry at a given position. + * + * If label is NULL, the entry will be a separator. Many functions won't work + * for an entry that is a separator. + * + * An entry does not need to be destroyed; it will be destroyed with the tray. + * + * \param menu the menu to append the entry to. + * \param pos the desired position for the new entry. Entries at or following + * this place will be moved. If pos is -1, the entry is appended. + * \param label the text to be displayed on the entry, or NULL for a separator. + * \param flags a combination of flags, some of which are mandatory. + * \returns the newly created entry, or NULL if pos is out of bounds. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_TrayEntryFlags + * \sa SDL_GetTrayEntries + * \sa SDL_RemoveTrayEntry + * \sa SDL_GetTrayEntryParent + */ +extern SDL_DECLSPEC SDL_TrayEntry *SDLCALL SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags); + +/** + * Sets the label of an entry. + * + * An entry cannot change between a separator and an ordinary entry; that is, + * it is not possible to set a non-NULL label on an entry that has a NULL label + * (separators), or to set a NULL label to an entry that has a non-NULL label. + * The function will silently fail if that happens. + * + * \param entry the entry to be updated. + * \param label the new label for the entry. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + * \sa SDL_GetTrayEntryLabel + */ +extern SDL_DECLSPEC void SDLCALL SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label); + +/** + * Gets the label of an entry. + * + * If the returned value is NULL, the entry is a separator. + * + * \param entry the entry to be read. + * \returns the label of the entry. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + * \sa SDL_SetTrayEntryLabel + */ +extern SDL_DECLSPEC const char *SDLCALL SDL_GetTrayEntryLabel(SDL_TrayEntry *entry); + +/** + * Sets whether or not an entry is checked. + * + * The entry must have been created with the SDL_TRAYENTRY_CHECKBOX flag. + * + * \param entry the entry to be updated. + * \param checked SDL_TRUE if the entry should be checked; SDL_FALSE otherwise. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + * \sa SDL_GetTrayEntryChecked + */ +extern SDL_DECLSPEC void SDLCALL SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked); + +/** + * Gets whether or not an entry is checked. + * + * The entry must have been created with the SDL_TRAYENTRY_CHECKBOX flag. + * + * \param entry the entry to be read. + * \returns SDL_TRUE if the entry is checked; SDL_FALSE otherwise. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + * \sa SDL_SetTrayEntryChecked + */ +extern SDL_DECLSPEC bool SDLCALL SDL_GetTrayEntryChecked(SDL_TrayEntry *entry); + +/** + * Sets whether or not an entry is enabled. + * + * \param entry the entry to be updated. + * \param enabled SDL_TRUE if the entry should be enabled; SDL_FALSE otherwise. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + * \sa SDL_GetTrayEntryEnabled + */ +extern SDL_DECLSPEC void SDLCALL SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled); + +/** + * Gets whether or not an entry is enabled. + * + * \param entry the entry to be read. + * \returns SDL_TRUE if the entry is enabled; SDL_FALSE otherwise. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + * \sa SDL_SetTrayEntryEnabled + */ +extern SDL_DECLSPEC bool SDLCALL SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry); + +/** + * Sets a callback to be invoked when the entry is selected. + * + * \param entry the entry to be updated. + * \param callback a callback to be invoked when the entry is selected. + * \param userdata an optional pointer to pass extra data to the callback when + * it will be invoked. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetTrayEntries + * \sa SDL_InsertTrayEntryAt + */ +extern SDL_DECLSPEC void SDLCALL SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata); + +/** + * Destroys a tray object. + * + * This also destroys all associated menus and entries. + * + * \param tray the tray icon to be destroyed. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTray + */ +extern SDL_DECLSPEC void SDLCALL SDL_DestroyTray(SDL_Tray *tray); + +/** + * Gets the menu contianing a certain tray entry. + * + * \param entry the entry for which to get the parent menu. + * \returns the parent menu. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_InsertTrayEntryAt + */ +extern SDL_DECLSPEC SDL_TrayMenu *SDLCALL SDL_GetTrayEntryParent(SDL_TrayEntry *entry); + +/** + * Gets the entry for which the menu is a submenu, if the current menu is a + * submenu. + * + * Either this function or SDL_GetTrayMenuParentTray() will return non-NULL for + * any given menu. + * + * \param menu the menu for which to get the parent entry. + * \returns the parent entry, or NULL if this menu is not a submenu. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTraySubmenu + * \sa SDL_GetTrayMenuParentTray + */ +extern SDL_DECLSPEC SDL_TrayEntry *SDLCALL SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu); + +/** + * Gets the tray for which this menu is the first-level menu, if the current + * menu isn't a submenu. + * + * Either this function or SDL_GetTrayMenuParentEntry() will return non-NULL for + * any given menu. + * + * \param menu the menu for which to get the parent enttrayry. + * \returns the parent tray, or NULL if this menu is a submenu. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CreateTrayMenu + * \sa SDL_GetTrayMenuParentEntry + */ +extern SDL_DECLSPEC SDL_Tray *SDLCALL SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu); + +/* Ends C function definitions when using C++ */ +#ifdef __cplusplus +} +#endif +#include + +#endif /* SDL_tray_h_ */ diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index f3fe1c33c2..37a018e75f 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1208,6 +1208,27 @@ SDL3_0.0.0 { SDL_RenderTextureAffine; SDL_WaitAndAcquireGPUSwapchainTexture; SDL_RenderDebugTextFormat; + SDL_CreateTray; + SDL_SetTrayIcon; + SDL_SetTrayTooltip; + SDL_CreateTrayMenu; + SDL_CreateTraySubmenu; + SDL_GetTrayMenu; + SDL_GetTraySubmenu; + SDL_GetTrayEntries; + SDL_RemoveTrayEntry; + SDL_InsertTrayEntryAt; + SDL_SetTrayEntryLabel; + SDL_GetTrayEntryLabel; + SDL_SetTrayEntryChecked; + SDL_GetTrayEntryChecked; + SDL_SetTrayEntryEnabled; + SDL_GetTrayEntryEnabled; + SDL_SetTrayEntryCallback; + SDL_DestroyTray; + SDL_GetTrayEntryParent; + SDL_GetTrayMenuParentEntry; + SDL_GetTrayMenuParentTray; # extra symbols go here (don't modify this line) local: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index 12e77270ca..9fa340a26b 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1233,3 +1233,24 @@ #define SDL_RenderTextureAffine SDL_RenderTextureAffine_REAL #define SDL_WaitAndAcquireGPUSwapchainTexture SDL_WaitAndAcquireGPUSwapchainTexture_REAL #define SDL_RenderDebugTextFormat SDL_RenderDebugTextFormat_REAL +#define SDL_CreateTray SDL_CreateTray_REAL +#define SDL_SetTrayIcon SDL_SetTrayIcon_REAL +#define SDL_SetTrayTooltip SDL_SetTrayTooltip_REAL +#define SDL_CreateTrayMenu SDL_CreateTrayMenu_REAL +#define SDL_CreateTraySubmenu SDL_CreateTraySubmenu_REAL +#define SDL_GetTrayMenu SDL_GetTrayMenu_REAL +#define SDL_GetTraySubmenu SDL_GetTraySubmenu_REAL +#define SDL_GetTrayEntries SDL_GetTrayEntries_REAL +#define SDL_RemoveTrayEntry SDL_RemoveTrayEntry_REAL +#define SDL_InsertTrayEntryAt SDL_InsertTrayEntryAt_REAL +#define SDL_SetTrayEntryLabel SDL_SetTrayEntryLabel_REAL +#define SDL_GetTrayEntryLabel SDL_GetTrayEntryLabel_REAL +#define SDL_SetTrayEntryChecked SDL_SetTrayEntryChecked_REAL +#define SDL_GetTrayEntryChecked SDL_GetTrayEntryChecked_REAL +#define SDL_SetTrayEntryEnabled SDL_SetTrayEntryEnabled_REAL +#define SDL_GetTrayEntryEnabled SDL_GetTrayEntryEnabled_REAL +#define SDL_SetTrayEntryCallback SDL_SetTrayEntryCallback_REAL +#define SDL_DestroyTray SDL_DestroyTray_REAL +#define SDL_GetTrayEntryParent SDL_GetTrayEntryParent_REAL +#define SDL_GetTrayMenuParentEntry SDL_GetTrayMenuParentEntry_REAL +#define SDL_GetTrayMenuParentTray SDL_GetTrayMenuParentTray_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 189158744e..eec6229fff 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1241,3 +1241,24 @@ SDL_DYNAPI_PROC(bool,SDL_WaitAndAcquireGPUSwapchainTexture,(SDL_GPUCommandBuffer #ifndef SDL_DYNAPI_PROC_NO_VARARGS SDL_DYNAPI_PROC(bool,SDL_RenderDebugTextFormat,(SDL_Renderer *a,float b,float c,SDL_PRINTF_FORMAT_STRING const char *d,...),(a,b,c,d),return) #endif +SDL_DYNAPI_PROC(SDL_Tray*,SDL_CreateTray,(SDL_Surface *a,const char *b),(a,b),return) +SDL_DYNAPI_PROC(void,SDL_SetTrayIcon,(SDL_Tray *a,SDL_Surface *b),(a,b),) +SDL_DYNAPI_PROC(void,SDL_SetTrayTooltip,(SDL_Tray *a,const char *b),(a,b),) +SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_CreateTrayMenu,(SDL_Tray *a),(a),return) +SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_CreateTraySubmenu,(SDL_TrayEntry *a),(a),return) +SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_GetTrayMenu,(SDL_Tray *a),(a),return) +SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_GetTraySubmenu,(SDL_TrayEntry *a),(a),return) +SDL_DYNAPI_PROC(const SDL_TrayEntry**,SDL_GetTrayEntries,(SDL_TrayMenu *a,int *b),(a,b),return) +SDL_DYNAPI_PROC(void,SDL_RemoveTrayEntry,(SDL_TrayEntry *a),(a),) +SDL_DYNAPI_PROC(SDL_TrayEntry*,SDL_InsertTrayEntryAt,(SDL_TrayMenu *a,int b,const char *c,SDL_TrayEntryFlags d),(a,b,c,d),return) +SDL_DYNAPI_PROC(void,SDL_SetTrayEntryLabel,(SDL_TrayEntry *a,const char *b),(a,b),) +SDL_DYNAPI_PROC(const char*,SDL_GetTrayEntryLabel,(SDL_TrayEntry *a),(a),return) +SDL_DYNAPI_PROC(void,SDL_SetTrayEntryChecked,(SDL_TrayEntry *a,bool b),(a,b),) +SDL_DYNAPI_PROC(bool,SDL_GetTrayEntryChecked,(SDL_TrayEntry *a),(a),return) +SDL_DYNAPI_PROC(void,SDL_SetTrayEntryEnabled,(SDL_TrayEntry *a,bool b),(a,b),) +SDL_DYNAPI_PROC(bool,SDL_GetTrayEntryEnabled,(SDL_TrayEntry *a),(a),return) +SDL_DYNAPI_PROC(void,SDL_SetTrayEntryCallback,(SDL_TrayEntry *a,SDL_TrayCallback b,void *c),(a,b,c),) +SDL_DYNAPI_PROC(void,SDL_DestroyTray,(SDL_Tray *a),(a),) +SDL_DYNAPI_PROC(SDL_TrayMenu*,SDL_GetTrayEntryParent,(SDL_TrayEntry *a),(a),return) +SDL_DYNAPI_PROC(SDL_TrayEntry*,SDL_GetTrayMenuParentEntry,(SDL_TrayMenu *a),(a),return) +SDL_DYNAPI_PROC(SDL_Tray*,SDL_GetTrayMenuParentTray,(SDL_TrayMenu *a),(a),return) diff --git a/src/tray/cocoa/SDL_tray.m b/src/tray/cocoa/SDL_tray.m new file mode 100644 index 0000000000..515ee6527c --- /dev/null +++ b/src/tray/cocoa/SDL_tray.m @@ -0,0 +1,458 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + 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 + +#include "../../video/SDL_surface_c.h" + +/* applicationDockMenu */ + +struct SDL_TrayMenu { + NSMenu *nsmenu; + + size_t nEntries; + SDL_TrayEntry **entries; + + SDL_Tray *parent_tray; + SDL_TrayEntry *parent_entry; +}; + +struct SDL_TrayEntry { + NSMenuItem *nsitem; + + SDL_TrayEntryFlags flags; + SDL_TrayCallback callback; + void *userdata; + SDL_TrayMenu *submenu; + + SDL_TrayMenu *parent; +}; + +struct SDL_Tray { + NSStatusBar *statusBar; + NSStatusItem *statusItem; + + SDL_TrayMenu *menu; +}; + +static NSApplication *app = NULL; + +@interface AppDelegate: NSObject + - (IBAction)menu:(id)sender; +@end + +@implementation AppDelegate{} + - (IBAction)menu:(id)sender + { + SDL_TrayEntry *entry = [[sender representedObject] pointerValue]; + + if (!entry) { + return; + } + + if (entry->flags & SDL_TRAYENTRY_CHECKBOX) { + SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry)); + } + + if (entry->callback) { + entry->callback(entry->userdata, entry); + } + } +@end + +static void DestroySDLMenu(SDL_TrayMenu *menu) +{ + for (int i = 0; i < menu->nEntries; i++) { + if (menu->entries[i] && menu->entries[i]->submenu) { + DestroySDLMenu(menu->entries[i]->submenu); + } + SDL_free(menu->entries[i]); + } + + SDL_free(menu->entries); + + if (menu->parent_entry) { + [menu->parent_entry->parent->nsmenu setSubmenu:nil forItem:menu->parent_entry->nsitem]; + } else if (menu->parent_tray) { + [menu->parent_tray->statusItem setMenu:nil]; + } + + SDL_free(menu); +} + +SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) +{ + SDL_Tray *tray = (SDL_Tray *) SDL_malloc(sizeof(SDL_Tray)); + + AppDelegate *delegate = [[AppDelegate alloc] init]; + app = [NSApplication sharedApplication]; + [app setDelegate:delegate]; + + if (!tray) { + return NULL; + } + + SDL_memset((void *) tray, 0, sizeof(*tray)); + + tray->statusItem = nil; + tray->statusBar = [NSStatusBar systemStatusBar]; + tray->statusItem = [tray->statusBar statusItemWithLength:NSVariableStatusItemLength]; + [app activateIgnoringOtherApps:TRUE]; + + if (tooltip) { + tray->statusItem.button.toolTip = [NSString stringWithUTF8String:tooltip]; + } else { + tray->statusItem.button.toolTip = nil; + } + + if (icon) { + SDL_Surface *iconfmt = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32); + if (!iconfmt) { + goto skip_putting_an_icon; + } + + NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(unsigned char **)&iconfmt->pixels + pixelsWide:iconfmt->w + pixelsHigh:iconfmt->h + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bytesPerRow:iconfmt->pitch + bitsPerPixel:32]; + NSImage *iconimg = [[NSImage alloc] initWithSize:NSMakeSize(iconfmt->w, iconfmt->h)]; + [iconimg addRepresentation:bitmap]; + + /* A typical icon size is 22x22 on macOS. Failing to resize the icon + may give oversized status bar buttons. */ + NSImage *iconimg22 = [[NSImage alloc] initWithSize:NSMakeSize(22, 22)]; + [iconimg22 lockFocus]; + [iconimg setSize:NSMakeSize(22, 22)]; + [iconimg drawInRect:NSMakeRect(0, 0, 22, 22)]; + [iconimg22 unlockFocus]; + + tray->statusItem.button.image = iconimg22; + + SDL_DestroySurface(iconfmt); + } + +skip_putting_an_icon: + return tray; +} + +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + if (!icon) { + tray->statusItem.button.image = nil; + return; + } + + SDL_Surface *iconfmt = SDL_ConvertSurface(icon, SDL_PIXELFORMAT_RGBA32); + if (!iconfmt) { + tray->statusItem.button.image = nil; + return; + } + + NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(unsigned char **)&iconfmt->pixels + pixelsWide:iconfmt->w + pixelsHigh:iconfmt->h + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bytesPerRow:iconfmt->pitch + bitsPerPixel:32]; + NSImage *iconimg = [[NSImage alloc] initWithSize:NSMakeSize(iconfmt->w, iconfmt->h)]; + [iconimg addRepresentation:bitmap]; + + /* A typical icon size is 22x22 on macOS. Failing to resize the icon + may give oversized status bar buttons. */ + NSImage *iconimg22 = [[NSImage alloc] initWithSize:NSMakeSize(22, 22)]; + [iconimg22 lockFocus]; + [iconimg setSize:NSMakeSize(22, 22)]; + [iconimg drawInRect:NSMakeRect(0, 0, 22, 22)]; + [iconimg22 unlockFocus]; + + tray->statusItem.button.image = iconimg22; + + SDL_DestroySurface(iconfmt); +} + +void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) +{ + if (tooltip) { + tray->statusItem.button.toolTip = [NSString stringWithUTF8String:tooltip]; + } else { + tray->statusItem.button.toolTip = nil; + } +} + +SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) +{ + SDL_TrayMenu *menu = SDL_malloc(sizeof(SDL_TrayMenu)); + + if (!menu) { + return NULL; + } + + SDL_memset((void *) menu, 0, sizeof(*menu)); + + NSMenu *nsmenu = [[NSMenu alloc] init]; + [nsmenu setAutoenablesItems:FALSE]; + + [tray->statusItem setMenu:nsmenu]; + + tray->menu = menu; + menu->nsmenu = nsmenu; + menu->nEntries = 0; + menu->entries = NULL; + menu->parent_tray = tray; + menu->parent_entry = NULL; + + return menu; +} + +SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) +{ + return tray->menu; +} + +SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) +{ + if (entry->submenu) { + SDL_SetError("Tray entry submenu already exists"); + return NULL; + } + + if (!(entry->flags & SDL_TRAYENTRY_SUBMENU)) { + SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU"); + return NULL; + } + + SDL_TrayMenu *menu = SDL_malloc(sizeof(SDL_TrayMenu)); + + if (!menu) { + return NULL; + } + + SDL_memset((void *) menu, 0, sizeof(*menu)); + + NSMenu *nsmenu = [[NSMenu alloc] init]; + [nsmenu setAutoenablesItems:FALSE]; + + entry->submenu = menu; + menu->nsmenu = nsmenu; + menu->nEntries = 0; + menu->entries = NULL; + menu->parent_tray = NULL; + menu->parent_entry = entry; + + [entry->parent->nsmenu setSubmenu:nsmenu forItem:entry->nsitem]; + + return menu; +} + +SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) +{ + return entry->submenu; +} + +const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size) +{ + if (size) { + *size = menu->nEntries; + } + + return (const SDL_TrayEntry **) menu->entries; +} + +void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) +{ + if (!entry) { + return; + } + + SDL_TrayMenu *menu = entry->parent; + + bool found = false; + for (int i = 0; i < menu->nEntries - 1; i++) { + if (menu->entries[i] == entry) { + found = true; + } + + if (found) { + menu->entries[i] = menu->entries[i + 1]; + } + } + + if (entry->submenu) { + DestroySDLMenu(entry->submenu); + } + + menu->nEntries--; + SDL_TrayEntry ** new_entries = SDL_realloc(menu->entries, menu->nEntries * sizeof(SDL_TrayEntry *)); + + /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */ + if (new_entries) { + menu->entries = new_entries; + } + + [menu->nsmenu removeItem:entry->nsitem]; + + SDL_free(entry); +} + +SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) +{ + if (pos < -1 || pos > (int) menu->nEntries) { + SDL_InvalidParamError("pos"); + return NULL; + } + + if (pos == -1) { + pos = menu->nEntries; + } + + SDL_TrayEntry *entry = SDL_malloc(sizeof(SDL_TrayEntry)); + + if (!entry) { + return NULL; + } + + SDL_memset((void *) entry, 0, sizeof(*entry)); + + SDL_TrayEntry **new_entries = (SDL_TrayEntry **) SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(SDL_TrayEntry *)); + + if (!new_entries) { + SDL_free(entry); + return NULL; + } + + menu->entries = new_entries; + menu->nEntries++; + + for (int i = menu->nEntries - 1; i > pos; i--) { + menu->entries[i] = menu->entries[i - 1]; + } + + new_entries[pos] = entry; + + NSMenuItem *nsitem; + if (label == NULL) { + nsitem = [NSMenuItem separatorItem]; + } else { + nsitem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label] action:@selector(menu:) keyEquivalent:@""]; + [nsitem setEnabled:((flags & SDL_TRAYENTRY_DISABLED) ? FALSE : TRUE)]; + [nsitem setState:((flags & SDL_TRAYENTRY_CHECKED) ? NSControlStateValueOn : NSControlStateValueOff)]; + [nsitem setRepresentedObject:[NSValue valueWithPointer:entry]]; + } + + [menu->nsmenu insertItem:nsitem atIndex:pos]; + + entry->nsitem = nsitem; + entry->flags = flags; + entry->callback = NULL; + entry->userdata = NULL; + entry->submenu = NULL; + entry->parent = menu; + + return entry; +} + +void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) +{ + [entry->nsitem setTitle:[NSString stringWithUTF8String:label]]; +} + +const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) +{ + return [[entry->nsitem title] UTF8String]; +} + +void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) +{ + [entry->nsitem setState:(checked ? NSControlStateValueOn : NSControlStateValueOff)]; +} + +bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) +{ + return entry->nsitem.state == NSControlStateValueOn; +} + +void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Cannot update check for entry not created with SDL_TRAYENTRY_CHECKBOX"); + return; + } + + [entry->nsitem setEnabled:(enabled ? YES : NO)]; +} + +bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Cannot fetch check for entry not created with SDL_TRAYENTRY_CHECKBOX"); + return false; + } + + return entry->nsitem.enabled; +} + +void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) +{ + entry->callback = callback; + entry->userdata = userdata; +} + +SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) +{ + return entry->parent; +} + +SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) +{ + return menu->parent_entry; +} + +SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) +{ + return menu->parent_tray; +} + +void SDL_DestroyTray(SDL_Tray *tray) +{ + if (!tray) { + return; + } + + [[NSStatusBar systemStatusBar] removeStatusItem:tray->statusItem]; + + if (tray->menu) { + DestroySDLMenu(tray->menu); + } + + SDL_free(tray); +} diff --git a/src/tray/dummy/SDL_tray.c b/src/tray/dummy/SDL_tray.c new file mode 100644 index 0000000000..3a105ad46a --- /dev/null +++ b/src/tray/dummy/SDL_tray.c @@ -0,0 +1,139 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + 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" + +SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) +{ + SDL_Unsupported(); + return NULL; +} + +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + SDL_Unsupported(); +} + +void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) +{ + SDL_Unsupported(); +} + +SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) +{ + SDL_Unsupported(); + return NULL; +} + +SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) +{ + SDL_Unsupported(); + return NULL; +} + +SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) +{ + SDL_Unsupported(); + return NULL; +} + +SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) +{ + return NULL; +} + +const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size) +{ + SDL_Unsupported(); + return NULL; +} + +void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) +{ + SDL_Unsupported(); +} + +SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) +{ + SDL_Unsupported(); + return NULL; +} + +void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) +{ + SDL_Unsupported(); +} + +const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) +{ + SDL_Unsupported(); + return NULL; +} + +void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) +{ + SDL_Unsupported(); +} + +bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) +{ + SDL_Unsupported(); + return false; +} + +void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) +{ + SDL_Unsupported(); +} + +bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) +{ + SDL_Unsupported(); + return false; +} + +void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) +{ + SDL_Unsupported(); +} + +SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) +{ + SDL_Unsupported(); + return NULL; +} + +SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) +{ + SDL_Unsupported(); + return NULL; +} + +SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) +{ + SDL_Unsupported(); + return NULL; +} + +void SDL_DestroyTray(SDL_Tray *tray) +{ + SDL_Unsupported(); +} diff --git a/src/tray/unix/SDL_tray.c b/src/tray/unix/SDL_tray.c new file mode 100644 index 0000000000..b2e81640d9 --- /dev/null +++ b/src/tray/unix/SDL_tray.c @@ -0,0 +1,664 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + 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 + +/* getpid() */ +#include + +/* APPINDICATOR_HEADER is not exposed as a build setting, but the code has been + written nevertheless to make future maintenance easier. */ +#ifdef APPINDICATOR_HEADER +#include APPINDICATOR_HEADER +#else +/* ------------------------------------------------------------------------- */ +/* BEGIN THIRD-PARTY HEADER CONTENT */ +/* ------------------------------------------------------------------------- */ +/* Glib 2.0 */ + +typedef unsigned long gulong; +typedef void* gpointer; +typedef char gchar; +typedef int gint; +typedef unsigned int guint; +typedef gint gboolean; +typedef void (*GCallback)(void); +typedef struct _GClosure GClosure; +typedef void (*GClosureNotify) (gpointer data, GClosure *closure); +typedef gboolean (*GSourceFunc) (gpointer user_data); +typedef enum +{ + G_CONNECT_AFTER = 1 << 0, + 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); + +#define g_signal_connect(instance, detailed_signal, c_handler, data) \ + g_signal_connect_data ((instance), (detailed_signal), (c_handler), (data), NULL, (GConnectFlags) 0) + +#define _G_TYPE_CIC(ip, gt, ct) ((ct*) ip) + +#define G_TYPE_CHECK_INSTANCE_CAST(instance, g_type, c_type) (_G_TYPE_CIC ((instance), (g_type), c_type)) + +#define G_CALLBACK(f) ((GCallback) (f)) + +#define FALSE 0 +#define TRUE 1 + +/* GTK 3.0 */ + +typedef struct _GtkMenu GtkMenu; +typedef struct _GtkMenuItem GtkMenuItem; +typedef struct _GtkMenuShell GtkMenuShell; +typedef struct _GtkWidget GtkWidget; +typedef struct _GtkCheckMenuItem GtkCheckMenuItem; + +gboolean (*gtk_init_check)(int *argc, char ***argv); +void (*gtk_main)(void); + +GtkWidget* (*gtk_menu_new)(void); +GtkWidget* (*gtk_separator_menu_item_new)(void); +GtkWidget* (*gtk_menu_item_new_with_label)(const gchar *label); +void (*gtk_menu_item_set_submenu)(GtkMenuItem *menu_item, GtkWidget *submenu); +GtkWidget* (*gtk_check_menu_item_new_with_label)(const gchar *label); +void (*gtk_check_menu_item_set_active)(GtkCheckMenuItem *check_menu_item, gboolean is_active); +void (*gtk_widget_set_sensitive)(GtkWidget *widget, gboolean sensitive); +void (*gtk_widget_show)(GtkWidget *widget); +void (*gtk_menu_shell_append)(GtkMenuShell *menu_shell, GtkWidget *child); +void (*gtk_menu_shell_insert)(GtkMenuShell *menu_shell, GtkWidget *child, gint position); +void (*gtk_widget_destroy)(GtkWidget *widget); +const gchar *(*gtk_menu_item_get_label)(GtkMenuItem *menu_item); +void (*gtk_menu_item_set_label)(GtkMenuItem *menu_item, const gchar *label); +gboolean (*gtk_check_menu_item_get_active)(GtkCheckMenuItem *check_menu_item); +gboolean (*gtk_widget_get_sensitive)(GtkWidget *widget); + +#define GTK_MENU_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_MENU_ITEM, GtkMenuItem)) +#define GTK_WIDGET(widget) (G_TYPE_CHECK_INSTANCE_CAST ((widget), GTK_TYPE_WIDGET, GtkWidget)) +#define GTK_CHECK_MENU_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_CHECK_MENU_ITEM, GtkCheckMenuItem)) +#define GTK_MENU(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_MENU, GtkMenu)) + +/* AppIndicator */ + +typedef enum { + APP_INDICATOR_CATEGORY_APPLICATION_STATUS, + APP_INDICATOR_CATEGORY_COMMUNICATIONS, + APP_INDICATOR_CATEGORY_SYSTEM_SERVICES, + APP_INDICATOR_CATEGORY_HARDWARE, + APP_INDICATOR_CATEGORY_OTHER +} AppIndicatorCategory; + +typedef enum { + APP_INDICATOR_STATUS_PASSIVE, + APP_INDICATOR_STATUS_ACTIVE, + APP_INDICATOR_STATUS_ATTENTION +} AppIndicatorStatus; + +typedef struct _AppIndicator AppIndicator; +AppIndicator *(*app_indicator_new)(const gchar *id, const gchar *icon_name, AppIndicatorCategory category); +void (*app_indicator_set_status)(AppIndicator *self, AppIndicatorStatus status); +void (*app_indicator_set_icon)(AppIndicator *self, const gchar *icon_name); +void (*app_indicator_set_menu)(AppIndicator *self, GtkMenu *menu); +/* ------------------------------------------------------------------------- */ +/* END THIRD-PARTY HEADER CONTENT */ +/* ------------------------------------------------------------------------- */ +#endif + +static int main_gtk_thread(void *data) +{ + gtk_main(); + return 0; +} + +#ifdef APPINDICATOR_HEADER + +static void quit_gtk(void) +{ +} + +static bool init_gtk(void) +{ + SDL_DetachThread(SDL_CreateThread(main_gtk_thread, "tray gtk", NULL)); +} + +#else + +static bool gtk_is_init = false; + +static void *libappindicator = NULL; +static void *libgtk = NULL; +static void *libgdk = NULL; + +static void quit_gtk(void) +{ + if (libappindicator) { + dlclose(libappindicator); + libappindicator = NULL; + } + + if (libgtk) { + dlclose(libgtk); + libgtk = NULL; + } + + if (libgdk) { + dlclose(libgdk); + libgdk = NULL; + } + + gtk_is_init = false; +} + +const char *appindicator_names[] = { + "libayatana-appindicator3.so", + "libayatana-appindicator3.so.1", + "libayatana-appindicator.so", + "libappindicator3.so", + "libappindicator3.so.1", + "libappindicator.so", + "libappindicator.so.1", + NULL +}; + +const char *gtk_names[] = { + "libgtk-3.so", + "libgtk-3.so.0", + NULL +}; + +const char *gdk_names[] = { + "libgdk-3.so", + "libgdk-3.so.0", + NULL +}; + +static void *find_lib(const char **names) +{ + const char **name_ptr = names; + void *handle = NULL; + + do { + handle = dlopen(*name_ptr, RTLD_LAZY); + } while (*++name_ptr && !handle); + + return handle; +} + +static bool init_gtk(void) +{ + if (gtk_is_init) { + return true; + } + + libappindicator = find_lib(appindicator_names); + libgtk = find_lib(gtk_names); + libgdk = find_lib(gdk_names); + + if (!libappindicator || !libgtk || !libgdk) { + quit_gtk(); + return SDL_SetError("Could not load GTK/AppIndicator libraries"); + } + + gtk_init_check = dlsym(libgtk, "gtk_init_check"); + gtk_main = dlsym(libgtk, "gtk_main"); + gtk_menu_new = dlsym(libgtk, "gtk_menu_new"); + gtk_separator_menu_item_new = dlsym(libgtk, "gtk_separator_menu_item_new"); + gtk_menu_item_new_with_label = dlsym(libgtk, "gtk_menu_item_new_with_label"); + gtk_menu_item_set_submenu = dlsym(libgtk, "gtk_menu_item_set_submenu"); + gtk_check_menu_item_new_with_label = dlsym(libgtk, "gtk_check_menu_item_new_with_label"); + gtk_check_menu_item_set_active = dlsym(libgtk, "gtk_check_menu_item_set_active"); + gtk_widget_set_sensitive = dlsym(libgtk, "gtk_widget_set_sensitive"); + gtk_widget_show = dlsym(libgtk, "gtk_widget_show"); + gtk_menu_shell_append = dlsym(libgtk, "gtk_menu_shell_append"); + gtk_menu_shell_insert = dlsym(libgtk, "gtk_menu_shell_insert"); + gtk_widget_destroy = dlsym(libgtk, "gtk_widget_destroy"); + gtk_menu_item_get_label = dlsym(libgtk, "gtk_menu_item_get_label"); + gtk_menu_item_set_label = dlsym(libgtk, "gtk_menu_item_set_label"); + gtk_check_menu_item_get_active = dlsym(libgtk, "gtk_check_menu_item_get_active"); + gtk_widget_get_sensitive = dlsym(libgtk, "gtk_widget_get_sensitive"); + + g_signal_connect_data = dlsym(libgdk, "g_signal_connect_data"); + + app_indicator_new = dlsym(libappindicator, "app_indicator_new"); + app_indicator_set_status = dlsym(libappindicator, "app_indicator_set_status"); + app_indicator_set_icon = dlsym(libappindicator, "app_indicator_set_icon"); + app_indicator_set_menu = dlsym(libappindicator, "app_indicator_set_menu"); + + if (!gtk_init_check || + !gtk_main || + !gtk_menu_new || + !gtk_separator_menu_item_new || + !gtk_menu_item_new_with_label || + !gtk_menu_item_set_submenu || + !gtk_check_menu_item_new_with_label || + !gtk_check_menu_item_set_active || + !gtk_widget_set_sensitive || + !gtk_widget_show || + !gtk_menu_shell_append || + !gtk_menu_shell_insert || + !gtk_widget_destroy || + !g_signal_connect_data || + !app_indicator_new || + !app_indicator_set_status || + !app_indicator_set_icon || + !app_indicator_set_menu || + !gtk_menu_item_get_label || + !gtk_menu_item_set_label || + !gtk_check_menu_item_get_active || + !gtk_widget_get_sensitive) { + quit_gtk(); + return SDL_SetError("Could not load GTK/AppIndicator functions"); + } + + if (gtk_init_check(0, NULL) == FALSE) { + quit_gtk(); + return SDL_SetError("Could not init GTK"); + } + + gtk_is_init = true; + + SDL_DetachThread(SDL_CreateThread(main_gtk_thread, "tray gtk", NULL)); + + return true; +} +#endif + +struct SDL_TrayMenu { + GtkMenuShell *menu; + + size_t nEntries; + SDL_TrayEntry **entries; + + SDL_Tray *parent_tray; + SDL_TrayEntry *parent_entry; +}; + +struct SDL_TrayEntry { + SDL_TrayMenu *parent; + GtkWidget *item; + + /* Checkboxes are "activated" when programmatically checked/unchecked; this + is a workaround. */ + bool ignore_signal; + + SDL_TrayEntryFlags flags; + SDL_TrayCallback callback; + void *userdata; + SDL_TrayMenu *submenu; +}; + +struct SDL_Tray { + AppIndicator *indicator; + SDL_TrayMenu *menu; + char icon_path[256]; +}; + +static void call_callback(GtkMenuItem *item, gpointer ptr) +{ + SDL_TrayEntry *entry = ptr; + + /* Not needed with AppIndicator, may be needed with other frameworks */ + /* if (entry->flags & SDL_TRAYENTRY_CHECKBOX) { + SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry)); + } */ + + if (entry->ignore_signal) { + return; + } + + if (entry->callback) { + entry->callback(entry->userdata, entry); + } +} + +/* Since AppIndicator deals only in filenames, which are inherently subject to + timing attacks, don't bother generating a secure filename. */ +static bool get_tmp_filename(char *buffer, size_t size) +{ + static int count = 0; + + if (size < 64) { + return SDL_SetError("Can't create temporary file for icon: size %ld < 64", size); + } + + int would_have_written = SDL_snprintf(buffer, size, "/tmp/sdl_appindicator_icon_%d_%d.bmp", getpid(), count++); + + return would_have_written > 0 && would_have_written < size - 1; +} + +static const char *get_appindicator_id(void) +{ + static int count = 0; + static char buffer[256]; + + int would_have_written = SDL_snprintf(buffer, sizeof(buffer), "sdl-appindicator-%d-%d", getpid(), count++); + + if (would_have_written <= 0 || would_have_written >= sizeof(buffer) - 1) { + SDL_SetError("Couldn't fit %d bytes in buffer of size %ld", would_have_written, sizeof(buffer)); + return NULL; + } + + return buffer; +} + +static void DestroySDLMenu(SDL_TrayMenu *menu) +{ + for (int i = 0; i < menu->nEntries; i++) { + if (menu->entries[i] && menu->entries[i]->submenu) { + DestroySDLMenu(menu->entries[i]->submenu); + } + SDL_free(menu->entries[i]); + } + SDL_free(menu->entries); + SDL_free(menu); +} + +SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) +{ + if (init_gtk() != true) { + return NULL; + } + + SDL_Tray *tray = (SDL_Tray *) SDL_malloc(sizeof(SDL_Tray)); + + if (!tray) { + return NULL; + } + + SDL_memset((void *) tray, 0, sizeof(*tray)); + + get_tmp_filename(tray->icon_path, sizeof(tray->icon_path)); + SDL_SaveBMP(icon, tray->icon_path); + + tray->indicator = app_indicator_new(get_appindicator_id(), tray->icon_path, + APP_INDICATOR_CATEGORY_APPLICATION_STATUS); + + app_indicator_set_status(tray->indicator, APP_INDICATOR_STATUS_ACTIVE); + + return tray; +} + +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + if (*tray->icon_path) { + SDL_RemovePath(tray->icon_path); + } + + /* AppIndicator caches the icon files; always change filename to avoid caching */ + + if (icon) { + get_tmp_filename(tray->icon_path, sizeof(tray->icon_path)); + SDL_SaveBMP(icon, tray->icon_path); + app_indicator_set_icon(tray->indicator, tray->icon_path); + } else { + *tray->icon_path = '\0'; + app_indicator_set_icon(tray->indicator, NULL); + } +} + +void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) +{ + /* AppIndicator provides no tooltip support. */ +} + +SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) +{ + tray->menu = SDL_malloc(sizeof(SDL_TrayMenu)); + + if (!tray->menu) { + return NULL; + } + + tray->menu->menu = (GtkMenuShell *)gtk_menu_new(); + tray->menu->parent_tray = tray; + tray->menu->parent_entry = NULL; + tray->menu->nEntries = 0; + tray->menu->entries = NULL; + + app_indicator_set_menu(tray->indicator, GTK_MENU(tray->menu->menu)); + + return tray->menu; +} + +SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) +{ + return tray->menu; +} + +SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) +{ + if (entry->submenu) { + SDL_SetError("Tray entry submenu already exists"); + return NULL; + } + + if (!(entry->flags & SDL_TRAYENTRY_SUBMENU)) { + SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU"); + return NULL; + } + + entry->submenu = SDL_malloc(sizeof(SDL_TrayMenu)); + + if (!entry->submenu) { + return NULL; + } + + entry->submenu->menu = (GtkMenuShell *)gtk_menu_new(); + entry->submenu->parent_tray = NULL; + entry->submenu->parent_entry = entry; + entry->submenu->nEntries = 0; + entry->submenu->entries = NULL; + + gtk_menu_item_set_submenu(GTK_MENU_ITEM(entry->item), GTK_WIDGET(entry->submenu->menu)); + + return entry->submenu; +} + +SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) +{ + return entry->submenu; +} + +const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size) +{ + if (size) { + *size = menu->nEntries; + } + + return (const SDL_TrayEntry **) menu->entries; +} + +void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) +{ + if (!entry) { + return; + } + + SDL_TrayMenu *menu = entry->parent; + + bool found = false; + for (int i = 0; i < menu->nEntries - 1; i++) { + if (menu->entries[i] == entry) { + found = true; + } + + if (found) { + menu->entries[i] = menu->entries[i + 1]; + } + } + + if (entry->submenu) { + DestroySDLMenu(entry->submenu); + } + + menu->nEntries--; + SDL_TrayEntry ** new_entries = SDL_realloc(menu->entries, menu->nEntries * sizeof(SDL_TrayEntry *)); + + /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */ + if (new_entries) { + menu->entries = new_entries; + } + + gtk_widget_destroy(entry->item); + SDL_free(entry); +} + +SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) +{ + if (pos < -1 || pos > (int) menu->nEntries) { + SDL_InvalidParamError("pos"); + return NULL; + } + + if (pos == -1) { + pos = menu->nEntries; + } + + SDL_TrayEntry *entry = SDL_malloc(sizeof(SDL_TrayEntry)); + + if (!entry) { + return NULL; + } + + SDL_memset((void *) entry, 0, sizeof(*entry)); + entry->parent = menu; + entry->item = NULL; + entry->ignore_signal = false; + entry->flags = flags; + entry->callback = NULL; + entry->userdata = NULL; + entry->submenu = NULL; + + if (label == NULL) { + entry->item = gtk_separator_menu_item_new(); + } else if (flags & SDL_TRAYENTRY_CHECKBOX) { + entry->item = gtk_check_menu_item_new_with_label(label); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(entry->item), !!(flags & SDL_TRAYENTRY_CHECKED)); + } else { + entry->item = gtk_menu_item_new_with_label(label); + } + + gtk_widget_set_sensitive(entry->item, !(flags & SDL_TRAYENTRY_DISABLED)); + + SDL_TrayEntry **new_entries = (SDL_TrayEntry **) SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(SDL_TrayEntry *)); + + if (!new_entries) { + SDL_free(entry); + return NULL; + } + + menu->entries = new_entries; + menu->nEntries++; + + for (int i = menu->nEntries - 1; i > pos; i--) { + menu->entries[i] = menu->entries[i - 1]; + } + + new_entries[pos] = entry; + + gtk_widget_show(entry->item); + gtk_menu_shell_insert(menu->menu, entry->item, (pos == menu->nEntries) ? -1 : pos); + + g_signal_connect(entry->item, "activate", G_CALLBACK(call_callback), entry); + + return entry; +} + +void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) +{ + gtk_menu_item_set_label(GTK_MENU_ITEM(entry->item), label); +} + +const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) +{ + return gtk_menu_item_get_label(GTK_MENU_ITEM(entry->item)); +} + +void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Cannot update check for entry not created with SDL_TRAYENTRY_CHECKBOX"); + return; + } + + entry->ignore_signal = true; + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(entry->item), checked); + entry->ignore_signal = false; +} + +bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Cannot fetch check for entry not created with SDL_TRAYENTRY_CHECKBOX"); + return false; + } + + return gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(entry->item)); +} + +void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) +{ + gtk_widget_set_sensitive(entry->item, enabled); +} + +bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) +{ + return gtk_widget_get_sensitive(entry->item); +} + +void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) +{ + entry->callback = callback; + entry->userdata = userdata; +} + +SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) +{ + return entry->parent; +} + +SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) +{ + return menu->parent_entry; +} + +SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) +{ + return menu->parent_tray; +} + +void SDL_DestroyTray(SDL_Tray *tray) +{ + if (!tray) { + return; + } + + if (tray->menu) { + DestroySDLMenu(tray->menu); + } + + if (*tray->icon_path) { + SDL_RemovePath(tray->icon_path); + } + + SDL_free(tray); +} diff --git a/src/tray/windows/SDL_tray.c b/src/tray/windows/SDL_tray.c new file mode 100644 index 0000000000..a09a086ca4 --- /dev/null +++ b/src/tray/windows/SDL_tray.c @@ -0,0 +1,589 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + 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/windows/SDL_surface_utils.h" + +#include +#include +#include + +#include + +#define WM_TRAYICON (WM_USER + 1) + +struct SDL_TrayMenu { + HMENU hMenu; + + size_t nEntries; + SDL_TrayEntry **entries; + + SDL_Tray *parent_tray; + SDL_TrayEntry *parent_entry; +}; + +struct SDL_TrayEntry { + SDL_TrayMenu *parent; + UINT_PTR id; + + char label_cache[4096]; + SDL_TrayEntryFlags flags; + SDL_TrayCallback callback; + void *userdata; + SDL_TrayMenu *submenu; +}; + +struct SDL_Tray { + NOTIFYICONDATAW nid; + HWND hwnd; + HICON icon; + SDL_TrayMenu *menu; +}; + +static UINT_PTR get_next_id(void) +{ + static UINT_PTR next_id = 0; + return ++next_id; +} + +static SDL_TrayEntry *find_entry_in_menu(SDL_TrayMenu *menu, UINT_PTR id) +{ + for (size_t i = 0; i < menu->nEntries; i++) { + SDL_TrayEntry *entry = menu->entries[i]; + + if (entry->id == id) { + return entry; + } + + if (entry->submenu) { + SDL_TrayEntry *e = find_entry_in_menu(entry->submenu, id); + + if (e) { + return e; + } + } + } + + return NULL; +} + +static SDL_TrayEntry *find_entry_with_id(SDL_Tray *tray, UINT_PTR id) +{ + if (!tray->menu) { + return NULL; + } + + return find_entry_in_menu(tray->menu, id); +} + +LRESULT CALLBACK TrayWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + SDL_Tray *tray = (SDL_Tray *) GetWindowLongPtr(hwnd, GWLP_USERDATA); + SDL_TrayEntry *entry = NULL; + + if (!tray) { + return DefWindowProc(hwnd, uMsg, wParam, lParam); + } + + switch (uMsg) { + case WM_TRAYICON: + if (LOWORD(lParam) == WM_CONTEXTMENU || LOWORD(lParam) == WM_LBUTTONUP) { + SetForegroundWindow(hwnd); + TrackPopupMenu(tray->menu->hMenu, TPM_BOTTOMALIGN | TPM_RIGHTALIGN, GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam), 0, hwnd, NULL); + } + break; + + case WM_COMMAND: + entry = find_entry_with_id(tray, LOWORD(wParam)); + + if (entry && (entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetTrayEntryChecked(entry, !SDL_GetTrayEntryChecked(entry)); + } + + if (entry && entry->callback) { + entry->callback(entry->userdata, entry); + } + break; + + default: + return DefWindowProc(hwnd, uMsg, wParam, lParam); + } + return 0; +} + +static void DestroySDLMenu(SDL_TrayMenu *menu) +{ + for (size_t i = 0; i < menu->nEntries; i++) { + if (menu->entries[i] && menu->entries[i]->submenu) { + DestroySDLMenu(menu->entries[i]->submenu); + } + SDL_free(menu->entries[i]); + } + SDL_free(menu->entries); + DestroyMenu(menu->hMenu); + SDL_free(menu); +} + +static wchar_t *convert_label(const char *in) +{ + const char *c; + char *c2; + int len = 0; + + for (c = in; *c; c++) { + len += (*c == '&') ? 2 : 1; + } + + char *escaped = SDL_malloc(SDL_strlen(in) + len + 1); + + if (!escaped) { + return NULL; + } + + for (c = in, c2 = escaped; *c;) { + if (*c == '&') { + *c2++ = *c; + } + + *c2++ = *c++; + } + + *c2 = '\0'; + + int len_w = MultiByteToWideChar(CP_UTF8, 0, escaped, len + 1, NULL, 0); + wchar_t *out = (wchar_t *)SDL_malloc(len_w * sizeof(wchar_t)); + + if (!out) { + SDL_free(escaped); + return NULL; + } + + MultiByteToWideChar(CP_UTF8, 0, escaped, -1, out, len_w); + + SDL_free(escaped); + + return out; +} + +static void register_tray_window_class(void) +{ + static bool init = false; + + if (init) { + return; + } + + HINSTANCE hInstance = GetModuleHandle(NULL); + WNDCLASSW wc; + ZeroMemory(&wc, sizeof(WNDCLASS)); + wc.lpfnWndProc = TrayWindowProc; + wc.hInstance = hInstance; + wc.lpszClassName = L"SDLTrayRunner"; + + RegisterClassW(&wc); + + init = true; +} + +SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) +{ + SDL_Tray *tray = SDL_malloc(sizeof(SDL_Tray)); + + if (!tray) { + return NULL; + } + + tray->hwnd = NULL; + tray->menu = NULL; + + register_tray_window_class(); + + HINSTANCE hInstance = GetModuleHandle(NULL); + tray->hwnd = CreateWindowExW(0, L"SDLTrayRunner", NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, hInstance, NULL); + + ZeroMemory(&tray->nid, sizeof(NOTIFYICONDATAW)); + tray->nid.cbSize = sizeof(NOTIFYICONDATAW); + tray->nid.hWnd = tray->hwnd; + tray->nid.uID = (UINT) get_next_id(); + tray->nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP; + tray->nid.uCallbackMessage = WM_TRAYICON; + tray->nid.uVersion = NOTIFYICON_VERSION_4; + mbstowcs_s(NULL, tray->nid.szTip, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip), tooltip, _TRUNCATE); + + if (icon) { + tray->nid.hIcon = CreateIconFromSurface(icon); + + if (!tray->nid.hIcon) { + tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION); + } + + tray->icon = tray->nid.hIcon; + } else { + tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION); + tray->icon = tray->nid.hIcon; + } + + Shell_NotifyIconW(NIM_ADD, &tray->nid); + Shell_NotifyIconW(NIM_SETVERSION, &tray->nid); + + SetWindowLongPtr(tray->hwnd, GWLP_USERDATA, (LONG_PTR) tray); + + return tray; +} + +void SDL_SetTrayIcon(SDL_Tray *tray, SDL_Surface *icon) +{ + if (tray->icon) { + DestroyIcon(tray->icon); + } + + if (icon) { + tray->nid.hIcon = CreateIconFromSurface(icon); + + if (!tray->nid.hIcon) { + tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION); + } + + tray->icon = tray->nid.hIcon; + } else { + tray->nid.hIcon = LoadIcon(NULL, IDI_APPLICATION); + tray->icon = tray->nid.hIcon; + } + + Shell_NotifyIconW(NIM_MODIFY, &tray->nid); +} + +void SDL_SetTrayTooltip(SDL_Tray *tray, const char *tooltip) +{ + if (tooltip) { + mbstowcs_s(NULL, tray->nid.szTip, sizeof(tray->nid.szTip) / sizeof(*tray->nid.szTip), tooltip, _TRUNCATE); + } else { + tray->nid.szTip[0] = '\0'; + } + + Shell_NotifyIconW(NIM_MODIFY, &tray->nid); +} + +SDL_TrayMenu *SDL_CreateTrayMenu(SDL_Tray *tray) +{ + tray->menu = SDL_malloc(sizeof(SDL_TrayMenu)); + + if (!tray->menu) { + return NULL; + } + + SDL_memset((void *) tray->menu, 0, sizeof(*tray->menu)); + + tray->menu->hMenu = CreatePopupMenu(); + tray->menu->parent_tray = tray; + + return tray->menu; +} + +SDL_TrayMenu *SDL_GetTrayMenu(SDL_Tray *tray) +{ + return tray->menu; +} + +SDL_TrayMenu *SDL_CreateTraySubmenu(SDL_TrayEntry *entry) +{ + if (!entry->submenu) { + SDL_SetError("Cannot create submenu for entry not created with SDL_TRAYENTRY_SUBMENU"); + } + + return entry->submenu; +} + +SDL_TrayMenu *SDL_GetTraySubmenu(SDL_TrayEntry *entry) +{ + return entry->submenu; +} + +const SDL_TrayEntry **SDL_GetTrayEntries(SDL_TrayMenu *menu, int *size) +{ + if (size) { + *size = (int) menu->nEntries; + } + + return (const SDL_TrayEntry **) menu->entries; +} + +void SDL_RemoveTrayEntry(SDL_TrayEntry *entry) +{ + if (!entry) { + return; + } + + SDL_TrayMenu *menu = entry->parent; + + bool found = false; + for (size_t i = 0; i < menu->nEntries - 1; i++) { + if (menu->entries[i] == entry) { + found = true; + } + + if (found) { + menu->entries[i] = menu->entries[i + 1]; + } + } + + if (entry->submenu) { + DestroySDLMenu(entry->submenu); + } + + menu->nEntries--; + SDL_TrayEntry ** new_entries = SDL_realloc(menu->entries, menu->nEntries * sizeof(SDL_TrayEntry *)); + + /* Not sure why shrinking would fail, but even if it does, we can live with a "too big" array */ + if (new_entries) { + menu->entries = new_entries; + } + + if (!DeleteMenu(menu->hMenu, (UINT) entry->id, MF_BYCOMMAND)) { + /* This is somewhat useless since we don't return anything, but might help with eventual bugs */ + SDL_SetError("Couldn't destroy tray entry"); + } + + SDL_free(entry); +} + +SDL_TrayEntry *SDL_InsertTrayEntryAt(SDL_TrayMenu *menu, int pos, const char *label, SDL_TrayEntryFlags flags) +{ + if (pos < -1 || pos > (int) menu->nEntries) { + SDL_InvalidParamError("pos"); + return NULL; + } + + int windows_compatible_pos = pos; + + if (pos == -1) { + pos = (int) menu->nEntries; + } else if (pos == menu->nEntries) { + windows_compatible_pos = -1; + } + + SDL_TrayEntry *entry = SDL_malloc(sizeof(SDL_TrayEntry)); + + if (!entry) { + return NULL; + } + + wchar_t *label_w = NULL; + + if (label && !(label_w = convert_label(label))) { + SDL_free(entry); + return NULL; + } + + entry->parent = menu; + entry->flags = flags; + entry->callback = NULL; + entry->userdata = NULL; + entry->submenu = NULL; + SDL_snprintf(entry->label_cache, sizeof(entry->label_cache), "%s", label ? label : ""); + + if (label != NULL && flags & SDL_TRAYENTRY_SUBMENU) { + entry->submenu = SDL_malloc(sizeof(SDL_TrayMenu)); + + if (!entry->submenu) { + SDL_free(entry); + SDL_free(label_w); + return NULL; + } + + entry->submenu->hMenu = CreatePopupMenu(); + entry->submenu->nEntries = 0; + entry->submenu->entries = NULL; + + entry->id = (UINT_PTR) entry->submenu->hMenu; + } else { + entry->id = get_next_id(); + } + + SDL_TrayEntry **new_entries = (SDL_TrayEntry **) SDL_realloc(menu->entries, (menu->nEntries + 1) * sizeof(SDL_TrayEntry **)); + + if (!new_entries) { + SDL_free(entry); + SDL_free(label_w); + if (entry->submenu) { + DestroyMenu(entry->submenu->hMenu); + SDL_free(entry->submenu); + } + return NULL; + } + + menu->entries = new_entries; + menu->nEntries++; + + for (int i = (int) menu->nEntries - 1; i > pos; i--) { + menu->entries[i] = menu->entries[i - 1]; + } + + new_entries[pos] = entry; + + if (label == NULL) { + InsertMenuW(menu->hMenu, windows_compatible_pos, MF_SEPARATOR | MF_BYPOSITION, entry->id, NULL); + } else { + UINT mf = MF_STRING | MF_BYPOSITION; + if (flags & SDL_TRAYENTRY_SUBMENU) { + mf = MF_POPUP; + } + + if (flags & SDL_TRAYENTRY_DISABLED) { + mf |= MF_DISABLED | MF_GRAYED; + } + + if (flags & SDL_TRAYENTRY_CHECKED) { + mf |= MF_CHECKED; + } + + InsertMenuW(menu->hMenu, windows_compatible_pos, mf, entry->id, label_w); + + SDL_free(label_w); + } + + return entry; +} + +void SDL_SetTrayEntryLabel(SDL_TrayEntry *entry, const char *label) +{ + SDL_snprintf(entry->label_cache, sizeof(entry->label_cache), "%s", label); + + wchar_t *label_w = convert_label(label); + + if (!label_w) { + return; + } + + MENUITEMINFOW mii; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_STRING; + + mii.dwTypeData = label_w; + mii.cch = (UINT) wcslen(label_w); + + if (!SetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, TRUE, &mii)) { + SDL_SetError("Couldn't update tray entry label"); + } + + SDL_free(label_w); +} + +const char *SDL_GetTrayEntryLabel(SDL_TrayEntry *entry) +{ + return entry->label_cache; +} + +void SDL_SetTrayEntryChecked(SDL_TrayEntry *entry, bool checked) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Can't check/uncheck tray entry not created with SDL_TRAYENTRY_CHECKBOX"); + return; + } + + CheckMenuItem(entry->parent->hMenu, (UINT) entry->id, checked ? MF_CHECKED : MF_UNCHECKED); +} + +bool SDL_GetTrayEntryChecked(SDL_TrayEntry *entry) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Can't get check status of tray entry not created with SDL_TRAYENTRY_CHECKBOX"); + return false; + } + + MENUITEMINFOW mii; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_STATE; + + GetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, FALSE, &mii); + + return !!(mii.fState & MFS_CHECKED); +} + +void SDL_SetTrayEntryEnabled(SDL_TrayEntry *entry, bool enabled) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Cannot update check for entry not created with SDL_TRAYENTRY_CHECKBOX"); + return; + } + + EnableMenuItem(entry->parent->hMenu, (UINT) entry->id, MF_BYCOMMAND | (enabled ? MF_ENABLED : (MF_DISABLED | MF_GRAYED))); +} + +bool SDL_GetTrayEntryEnabled(SDL_TrayEntry *entry) +{ + if (!(entry->flags & SDL_TRAYENTRY_CHECKBOX)) { + SDL_SetError("Cannot fetch check for entry not created with SDL_TRAYENTRY_CHECKBOX"); + return false; + } + + MENUITEMINFOW mii; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_STATE; + + GetMenuItemInfoW(entry->parent->hMenu, (UINT) entry->id, FALSE, &mii); + + return !!(mii.fState & MFS_ENABLED); +} + +void SDL_SetTrayEntryCallback(SDL_TrayEntry *entry, SDL_TrayCallback callback, void *userdata) +{ + entry->callback = callback; + entry->userdata = userdata; +} + +SDL_TrayMenu *SDL_GetTrayEntryParent(SDL_TrayEntry *entry) +{ + return entry->parent; +} + +SDL_TrayEntry *SDL_GetTrayMenuParentEntry(SDL_TrayMenu *menu) +{ + return menu->parent_entry; +} + +SDL_Tray *SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu) +{ + return menu->parent_tray; +} + +void SDL_DestroyTray(SDL_Tray *tray) +{ + if (!tray) { + return; + } + + Shell_NotifyIconW(NIM_DELETE, &tray->nid); + + if (tray->menu) { + DestroySDLMenu(tray->menu); + } + + if (tray->icon) { + DestroyIcon(tray->icon); + } + + if (tray->hwnd) { + DestroyWindow(tray->hwnd); + } + + SDL_free(tray); +} diff --git a/src/video/windows/SDL_surface_utils.c b/src/video/windows/SDL_surface_utils.c new file mode 100644 index 0000000000..bdcac106b8 --- /dev/null +++ b/src/video/windows/SDL_surface_utils.c @@ -0,0 +1,95 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + 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 "SDL_surface_utils.h" + +#include "../SDL_surface_c.h" + +HICON CreateIconFromSurface(SDL_Surface *surface) +{ + SDL_Surface *s = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32); + if (!s) { + return NULL; + } + + /* The dimensions will be needed after s is freed */ + const int width = s->w; + const int height = s->h; + + BITMAPINFO bmpInfo; + ZeroMemory(&bmpInfo, sizeof(BITMAPINFO)); + bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmpInfo.bmiHeader.biWidth = width; + bmpInfo.bmiHeader.biHeight = -height; /* Top-down bitmap */ + bmpInfo.bmiHeader.biPlanes = 1; + bmpInfo.bmiHeader.biBitCount = 32; + bmpInfo.bmiHeader.biCompression = BI_RGB; + + HDC hdc = GetDC(NULL); + void* pBits = NULL; + HBITMAP hBitmap = CreateDIBSection(hdc, &bmpInfo, DIB_RGB_COLORS, &pBits, NULL, 0); + if (!hBitmap) { + ReleaseDC(NULL, hdc); + SDL_DestroySurface(s); + return NULL; + } + + SDL_memcpy(pBits, s->pixels, width * height * 4); + + SDL_DestroySurface(s); + + HBITMAP hMask = CreateBitmap(width, height, 1, 1, NULL); + if (!hMask) { + DeleteObject(hBitmap); + ReleaseDC(NULL, hdc); + return NULL; + } + + HDC hdcMem = CreateCompatibleDC(hdc); + HGDIOBJ oldBitmap = SelectObject(hdcMem, hMask); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + BYTE* pixel = (BYTE*)pBits + (y * width + x) * 4; + BYTE alpha = pixel[3]; + COLORREF maskColor = (alpha == 0) ? RGB(0, 0, 0) : RGB(255, 255, 255); + SetPixel(hdcMem, x, y, maskColor); + } + } + + ICONINFO iconInfo; + iconInfo.fIcon = TRUE; + iconInfo.xHotspot = 0; + iconInfo.yHotspot = 0; + iconInfo.hbmMask = hMask; + iconInfo.hbmColor = hBitmap; + + HICON hIcon = CreateIconIndirect(&iconInfo); + + SelectObject(hdcMem, oldBitmap); + DeleteDC(hdcMem); + DeleteObject(hBitmap); + DeleteObject(hMask); + ReleaseDC(NULL, hdc); + + return hIcon; +} diff --git a/src/video/windows/SDL_surface_utils.h b/src/video/windows/SDL_surface_utils.h new file mode 100644 index 0000000000..5c05a4c8d1 --- /dev/null +++ b/src/video/windows/SDL_surface_utils.h @@ -0,0 +1,38 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + 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" + +#ifndef SDL_surface_utils_h_ +#define SDL_surface_utils_h_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern HICON CreateIconFromSurface(SDL_Surface *surface); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fe6d8e36f2..1a799482b0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -407,6 +407,7 @@ add_sdl_test_executable(testdialog SOURCES testdialog.c) add_sdl_test_executable(testtime SOURCES testtime.c) add_sdl_test_executable(testmanymouse SOURCES testmanymouse.c) add_sdl_test_executable(testmodal SOURCES testmodal.c) +add_sdl_test_executable(testtray SOURCES testtray.c) add_sdl_test_executable(testprocess diff --git a/test/sdl-test_round.bmp b/test/sdl-test_round.bmp new file mode 100644 index 0000000000000000000000000000000000000000..edb60497249fcb048281e1bed2126b735f968abd GIT binary patch literal 147594 zcmeI51)LVe|HlPsP(URldQ`~R% z?mF(_UY^0TcqY&09lXoYh@*{IG~w#+zDKu(Ti60ITfkui%tYgzG1rafjM3n4KNWBZ zM!_uDt*la2gMJP>Kr{s@NEVBqFuP{;Vs~>5RG}pQpR_%1g02s8Gjjz!8&6$SOk|~)M2}hj_az_ zQ>m|VaFHTK8mKfZS+ZnfN|`ccTxqJ~V6o@jb=16tvoyqh{u7wSaa zs3Ucy&W;}V7QP9Yf_ZFjm5>(H2z%%T?&*IDcmNj;^YAlne0O+f%w_z|Pyu(0+hDQ2 zFnWlO?G_3G8@(yCRf6FYY7c zC!BC5g=5a~H?HAY?(y&C8Np|AZ0U)2QwQooov0giRGpbCg7m<*@J)PMG%e5u;hdmG z*iE+}?%h}qZwx;Ve}GKMI1fJHBQjKKso3!yOo6cj4mbeZHF4MrV3Kk92;sMDuU@^b z?9->ueUCl%*mEzw_~QF-zWL^m4?p~H%%`7z`s?SPe?IHG@4lP&!w)|!`0>Xd|N7~t zpO%gqHEP-D(W94fJYvL%zjS`V(4j-;ef8B>vp)OmvnkyB)?06V_tHx*4d$78_Wis= z@9L&^wo@ITEk_$}j%h+&sWWxgcewe1Z^}^N8~IkgnQx~JXbaked5|_@jBxFy!*I)o zE#Ugk@i@_AJv=h}fg9&7<~4pEBHT1~7!tl5=D}I>Dpjgyv%=%w{{ngBxHOtJKH_t3tvc#-dvBIobv&Q`M&p&44#*Jq4=FMj7 z)~&WgA`#;X=eBIwVm58sWY(`=Z`Q6|YgVpYX_hWsY8EV5U}n#rZKh3|X2y>nzg{%7 zO!PE^cT)%Ia^898T}0giGyy#{akR@f>D!Pcple49d^>GGTfp-<{>aSr|x9IELbF|NTICQdZ-8%F4-+!CAbLSdpZsf?3YnWrG<0Fqe z@@$VDJua_Sty)LXK-T=gxB1@)Eg(D8@!Np52+j$#6?|bx2Wo_TvL(Ykye~2#`Vu?V z3lEmJiN-v}zPiWSmLx&UYyYIgHHGU5L_S

C#y;)$1~gFV^N0c}N_1?Ygbq)lmC+L*Sc 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 +#include +#include + +static void SDLCALL tray_quit(void *ptr, SDL_TrayEntry *entry) +{ + SDL_Event e; + e.type = SDL_EVENT_QUIT; + SDL_PushEvent(&e); +} + +static void SDLCALL apply_icon(void *ptr, const char * const *filelist, int filter) +{ + if (!*filelist) { + return; + } + + SDL_Surface *icon = SDL_LoadBMP(*filelist); + + if (!icon) { + SDL_Log("Couldn't load icon '%s': %s", *filelist, SDL_GetError()); + return; + } + + SDL_Tray *tray = (SDL_Tray *) ptr; + SDL_SetTrayIcon(tray, icon); + + SDL_DestroySurface(icon); +} + +static void SDLCALL change_icon(void *ptr, SDL_TrayEntry *entry) +{ + SDL_DialogFileFilter filters[] = { + { "BMP image files", "bmp" }, + { "All files", "*" }, + }; + + SDL_ShowOpenFileDialog(apply_icon, ptr, NULL, filters, 2, NULL, 0); +} + +static void SDLCALL print_entry(void *ptr, SDL_TrayEntry *entry) +{ + SDL_Log("Clicked on button '%s'\n", SDL_GetTrayEntryLabel(entry)); +} + +static void SDLCALL set_entry_enabled(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayEntry *target = (SDL_TrayEntry *) ptr; + SDL_SetTrayEntryEnabled(target, true); +} + +static void SDLCALL set_entry_disabled(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayEntry *target = (SDL_TrayEntry *) ptr; + SDL_SetTrayEntryEnabled(target, false); +} + +static void SDLCALL set_entry_checked(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayEntry *target = (SDL_TrayEntry *) ptr; + SDL_SetTrayEntryChecked(target, true); +} + +static void SDLCALL set_entry_unchecked(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayEntry *target = (SDL_TrayEntry *) ptr; + SDL_SetTrayEntryChecked(target, false); +} + +static void SDLCALL remove_entry(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayEntry *target = (SDL_TrayEntry *) ptr; + SDL_RemoveTrayEntry(target); + + SDL_TrayMenu *ctrl_submenu = SDL_GetTrayEntryParent(entry); + SDL_TrayEntry *ctrl_entry = SDL_GetTrayMenuParentEntry(ctrl_submenu); + + if (!ctrl_entry) { + SDL_Log("Attempt to remove a menu that isn't a submenu. This shouldn't happen.\n"); + return; + } + + SDL_RemoveTrayEntry(ctrl_entry); +} + +static void SDLCALL append_button_to(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayMenu *menu = (SDL_TrayMenu *) ptr; + SDL_TrayMenu *submenu; + SDL_TrayEntry *new_ctrl; + SDL_TrayEntry *new_ctrl_remove; + SDL_TrayEntry *new_ctrl_enabled; + SDL_TrayEntry *new_ctrl_disabled; + SDL_TrayEntry *new_example; + + new_ctrl = SDL_InsertTrayEntryAt(SDL_GetTrayEntryParent(entry), -1, "New button", SDL_TRAYENTRY_SUBMENU); + + if (!new_ctrl) { + SDL_Log("Couldn't insert entry in control tray: %s\n", SDL_GetError()); + return; + } + + /* ---------- */ + + submenu = SDL_CreateTraySubmenu(new_ctrl); + + if (!new_ctrl) { + SDL_Log("Couldn't create control tray entry submenu: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + /* ---------- */ + + new_example = SDL_InsertTrayEntryAt(menu, -1, "New button", SDL_TRAYENTRY_BUTTON); + + if (new_example == NULL) { + SDL_Log("Couldn't insert entry in example tray: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + SDL_SetTrayEntryCallback(new_example, print_entry, NULL); + + /* ---------- */ + + new_ctrl_remove = SDL_InsertTrayEntryAt(submenu, -1, "Remove", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_remove == NULL) { + SDL_Log("Couldn't insert new_ctrl_remove: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_remove, remove_entry, new_example); + + /* ---------- */ + + new_ctrl_enabled = SDL_InsertTrayEntryAt(submenu, -1, "Enable", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_enabled == NULL) { + SDL_Log("Couldn't insert new_ctrl_enabled: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_enabled, set_entry_enabled, new_example); + + /* ---------- */ + + new_ctrl_disabled = SDL_InsertTrayEntryAt(submenu, -1, "Disable", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_disabled == NULL) { + SDL_Log("Couldn't insert new_ctrl_disabled: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_disabled, set_entry_disabled, new_example); +} + +static void SDLCALL append_checkbox_to(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayMenu *menu = (SDL_TrayMenu *) ptr; + SDL_TrayMenu *submenu; + SDL_TrayEntry *new_ctrl; + SDL_TrayEntry *new_ctrl_remove; + SDL_TrayEntry *new_ctrl_enabled; + SDL_TrayEntry *new_ctrl_disabled; + SDL_TrayEntry *new_ctrl_checked; + SDL_TrayEntry *new_ctrl_unchecked; + SDL_TrayEntry *new_example; + + new_ctrl = SDL_InsertTrayEntryAt(SDL_GetTrayEntryParent(entry), -1, "New checkbox", SDL_TRAYENTRY_SUBMENU); + + if (!new_ctrl) { + SDL_Log("Couldn't insert entry in control tray: %s\n", SDL_GetError()); + return; + } + + /* ---------- */ + + submenu = SDL_CreateTraySubmenu(new_ctrl); + + if (!new_ctrl) { + SDL_Log("Couldn't create control tray entry submenu: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + /* ---------- */ + + new_example = SDL_InsertTrayEntryAt(menu, -1, "New checkbox", SDL_TRAYENTRY_CHECKBOX); + + if (new_example == NULL) { + SDL_Log("Couldn't insert entry in example tray: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + SDL_SetTrayEntryCallback(new_example, print_entry, NULL); + + /* ---------- */ + + new_ctrl_remove = SDL_InsertTrayEntryAt(submenu, -1, "Remove", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_remove == NULL) { + SDL_Log("Couldn't insert new_ctrl_remove: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_remove, remove_entry, new_example); + + /* ---------- */ + + new_ctrl_enabled = SDL_InsertTrayEntryAt(submenu, -1, "Enable", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_enabled == NULL) { + SDL_Log("Couldn't insert new_ctrl_enabled: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_enabled, set_entry_enabled, new_example); + + /* ---------- */ + + new_ctrl_disabled = SDL_InsertTrayEntryAt(submenu, -1, "Disable", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_disabled == NULL) { + SDL_Log("Couldn't insert new_ctrl_disabled: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_disabled, set_entry_disabled, new_example); + + /* ---------- */ + + new_ctrl_checked = SDL_InsertTrayEntryAt(submenu, -1, "Check", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_checked == NULL) { + SDL_Log("Couldn't insert new_ctrl_checked: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_checked, set_entry_checked, new_example); + + /* ---------- */ + + new_ctrl_unchecked = SDL_InsertTrayEntryAt(submenu, -1, "Uncheck", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_unchecked == NULL) { + SDL_Log("Couldn't insert new_ctrl_unchecked: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_unchecked, set_entry_unchecked, new_example); +} + +static void SDLCALL append_separator_to(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayMenu *menu = (SDL_TrayMenu *) ptr; + SDL_TrayMenu *submenu; + SDL_TrayEntry *new_ctrl; + SDL_TrayEntry *new_ctrl_remove; + SDL_TrayEntry *new_example; + + new_ctrl = SDL_InsertTrayEntryAt(SDL_GetTrayEntryParent(entry), -1, "[Separator]", SDL_TRAYENTRY_SUBMENU); + + if (!new_ctrl) { + SDL_Log("Couldn't insert entry in control tray: %s\n", SDL_GetError()); + return; + } + + /* ---------- */ + + submenu = SDL_CreateTraySubmenu(new_ctrl); + + if (!new_ctrl) { + SDL_Log("Couldn't create control tray entry submenu: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + /* ---------- */ + + new_example = SDL_InsertTrayEntryAt(menu, -1, NULL, SDL_TRAYENTRY_BUTTON); + + if (new_example == NULL) { + SDL_Log("Couldn't insert separator in example tray: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + /* ---------- */ + + new_ctrl_remove = SDL_InsertTrayEntryAt(submenu, -1, "Remove", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_remove == NULL) { + SDL_Log("Couldn't insert new_ctrl_remove: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_remove, remove_entry, new_example); +} + +static void SDLCALL append_submenu_to(void *ptr, SDL_TrayEntry *entry) +{ + SDL_TrayMenu *menu = (SDL_TrayMenu *) ptr; + SDL_TrayMenu *submenu; + SDL_TrayMenu *entry_submenu; + SDL_TrayEntry *new_ctrl; + SDL_TrayEntry *new_ctrl_remove; + SDL_TrayEntry *new_ctrl_enabled; + SDL_TrayEntry *new_ctrl_disabled; + SDL_TrayEntry *new_example; + + new_ctrl = SDL_InsertTrayEntryAt(SDL_GetTrayEntryParent(entry), -1, "New submenu", SDL_TRAYENTRY_SUBMENU); + + if (!new_ctrl) { + SDL_Log("Couldn't insert entry in control tray: %s\n", SDL_GetError()); + return; + } + + /* ---------- */ + + submenu = SDL_CreateTraySubmenu(new_ctrl); + + if (!new_ctrl) { + SDL_Log("Couldn't create control tray entry submenu: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + /* ---------- */ + + new_example = SDL_InsertTrayEntryAt(menu, -1, "New submenu", SDL_TRAYENTRY_SUBMENU); + + if (new_example == NULL) { + SDL_Log("Couldn't insert entry in example tray: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + return; + } + + SDL_SetTrayEntryCallback(new_example, print_entry, NULL); + + /* ---------- */ + + entry_submenu = SDL_CreateTraySubmenu(new_example); + + if (entry_submenu == NULL) { + SDL_Log("Couldn't create new entry submenu: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + /* ---------- */ + + new_ctrl_remove = SDL_InsertTrayEntryAt(submenu, -1, "Remove", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_remove == NULL) { + SDL_Log("Couldn't insert new_ctrl_remove: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_remove, remove_entry, new_example); + + /* ---------- */ + + new_ctrl_enabled = SDL_InsertTrayEntryAt(submenu, -1, "Enable", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_enabled == NULL) { + SDL_Log("Couldn't insert new_ctrl_enabled: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_enabled, set_entry_enabled, new_example); + + /* ---------- */ + + new_ctrl_disabled = SDL_InsertTrayEntryAt(submenu, -1, "Disable", SDL_TRAYENTRY_BUTTON); + + if (new_ctrl_disabled == NULL) { + SDL_Log("Couldn't insert new_ctrl_disabled: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(new_ctrl_disabled, set_entry_disabled, new_example); + + /* ---------- */ + + SDL_InsertTrayEntryAt(submenu, -1, NULL, 0); + + /* ---------- */ + + SDL_TrayEntry *entry_newbtn = SDL_InsertTrayEntryAt(submenu, -1, "Create button", SDL_TRAYENTRY_BUTTON); + + if (entry_newbtn == NULL) { + SDL_Log("Couldn't insert entry_newbtn: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(entry_newbtn, append_button_to, entry_submenu); + + /* ---------- */ + + SDL_TrayEntry *entry_newchk = SDL_InsertTrayEntryAt(submenu, -1, "Create checkbox", SDL_TRAYENTRY_BUTTON); + + if (entry_newchk == NULL) { + SDL_Log("Couldn't insert entry_newchk: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(entry_newchk, append_checkbox_to, entry_submenu); + + /* ---------- */ + + SDL_TrayEntry *entry_newsub = SDL_InsertTrayEntryAt(submenu, -1, "Create submenu", SDL_TRAYENTRY_BUTTON); + + if (entry_newsub == NULL) { + SDL_Log("Couldn't insert entry_newsub: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(entry_newsub, append_submenu_to, entry_submenu); + + /* ---------- */ + + SDL_TrayEntry *entry_newsep = SDL_InsertTrayEntryAt(submenu, -1, "Create separator", SDL_TRAYENTRY_BUTTON); + + if (entry_newsep == NULL) { + SDL_Log("Couldn't insert entry_newsep: %s\n", SDL_GetError()); + SDL_RemoveTrayEntry(new_ctrl); + SDL_RemoveTrayEntry(new_example); + return; + } + + SDL_SetTrayEntryCallback(entry_newsep, append_separator_to, entry_submenu); + + /* ---------- */ + + SDL_InsertTrayEntryAt(submenu, -1, NULL, 0); +} + +int main(int argc, char **argv) +{ + SDLTest_CommonState *state; + int i; + + /* Initialize test framework */ + state = SDLTest_CommonCreateState(argv, 0); + if (state == NULL) { + return 1; + } + + /* Parse commandline */ + for (i = 1; i < argc;) { + int consumed; + + consumed = SDLTest_CommonArg(state, i); + + if (consumed <= 0) { + static const char *options[] = { NULL }; + SDLTest_CommonLogUsage(state, argv[0], options); + return 1; + } + + i += consumed; + } + + if (!SDL_Init(SDL_INIT_VIDEO)) { + SDL_Log("SDL_Init failed (%s)", SDL_GetError()); + return 1; + } + + /* TODO: Resource paths? */ + SDL_Surface *icon = SDL_LoadBMP("../test/sdl-test_round.bmp"); + + if (!icon) { + SDL_Log("Couldn't load icon 1, proceeding without: %s", SDL_GetError()); + } + + SDL_Surface *icon2 = SDL_LoadBMP("../test/speaker.bmp"); + + if (!icon2) { + SDL_Log("Couldn't load icon 2, proceeding without: %s", SDL_GetError()); + } + + SDL_Tray *tray = SDL_CreateTray(icon, "SDL Tray control menu"); + + if (!tray) { + SDL_Log("Couldn't create control tray: %s", SDL_GetError()); + goto quit; + } + + SDL_Tray *tray2 = SDL_CreateTray(icon2, "SDL Tray example"); + + if (!tray2) { + SDL_Log("Couldn't create example tray: %s", SDL_GetError()); + goto clean_tray1; + } + + SDL_DestroySurface(icon); + SDL_DestroySurface(icon2); + +#define CHECK(name) \ + if (!name) { \ + SDL_Log("Couldn't create " #name ": %s", SDL_GetError()); \ + goto clean_all; \ + } + + SDL_TrayMenu *menu = SDL_CreateTrayMenu(tray); + CHECK(menu); + + SDL_TrayMenu *menu2 = SDL_CreateTrayMenu(tray2); + CHECK(menu2); + + SDL_TrayEntry *entry_quit = SDL_InsertTrayEntryAt(menu, -1, "Quit", SDL_TRAYENTRY_BUTTON); + CHECK(entry_quit); + + SDL_SetTrayEntryCallback(entry_quit, tray_quit, NULL); + + SDL_InsertTrayEntryAt(menu, -1, NULL, 0); + + SDL_TrayEntry *entry_icon = SDL_InsertTrayEntryAt(menu, -1, "Change icon", SDL_TRAYENTRY_BUTTON); + CHECK(entry_icon); + + SDL_SetTrayEntryCallback(entry_icon, change_icon, tray2); + + SDL_InsertTrayEntryAt(menu, -1, NULL, 0); + + SDL_TrayEntry *entry_newbtn = SDL_InsertTrayEntryAt(menu, -1, "Create button", SDL_TRAYENTRY_BUTTON); + CHECK(entry_newbtn); + + SDL_SetTrayEntryCallback(entry_newbtn, append_button_to, menu2); + + SDL_TrayEntry *entry_newchk = SDL_InsertTrayEntryAt(menu, -1, "Create checkbox", SDL_TRAYENTRY_BUTTON); + CHECK(entry_newchk); + + SDL_SetTrayEntryCallback(entry_newchk, append_checkbox_to, menu2); + + SDL_TrayEntry *entry_newsub = SDL_InsertTrayEntryAt(menu, -1, "Create submenu", SDL_TRAYENTRY_BUTTON); + CHECK(entry_newsub); + + SDL_SetTrayEntryCallback(entry_newsub, append_submenu_to, menu2); + + SDL_TrayEntry *entry_newsep = SDL_InsertTrayEntryAt(menu, -1, "Create separator", SDL_TRAYENTRY_BUTTON); + CHECK(entry_newsep); + + SDL_SetTrayEntryCallback(entry_newsep, append_separator_to, menu2); + + SDL_InsertTrayEntryAt(menu, -1, NULL, 0); + + SDL_Event e; + while (SDL_WaitEvent(&e)) { + if (e.type == SDL_EVENT_QUIT) { + break; + } + } + +clean_all: + SDL_DestroyTray(tray2); + +clean_tray1: + SDL_DestroyTray(tray); + +quit: + SDL_Quit(); + SDLTest_CommonDestroyState(state); + + return 0; +}